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 8e9decf37a..0b4033ce8f 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -7,9 +7,13 @@ #include #include #include +#include #include +#include + #include "ClipperUtils.hpp" +#include "AABBTreeIndirect.hpp" #include "BoundingBox.hpp" #include "ExPolygon.hpp" #include "Point.hpp" @@ -276,6 +280,7 @@ Polylines c2_to_polylines(const C2::Paths64 &paths) return out; } +// Only sizes the reserve of c2_to_expolygons(): for a tree Clipper2 misnested (see below) the count is approximate. size_t c2_count_expolygons(const C2::PolyPath64 &outer) { size_t cnt = 1; @@ -285,17 +290,90 @@ size_t c2_count_expolygons(const C2::PolyPath64 &outer) return cnt; } -void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out) +// Clipper2 sometimes puts a path at the wrong level of its PolyTree: a hole beside the outer path around it instead of +// under it, or an island at the level of holes. Taken as they are, a misplaced hole becomes a clockwise contour and its +// outer an expolygon that covers it; whole-layer booleans cancel the two, but anything handling expolygons one at a time +// sees the hole filled. So the orientation of a path decides its role: a clockwise path where a contour belongs is +// collected in stray_holes, for c2_attach_stray_holes() to give to the contour around it. +void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out, std::vector &stray_holes) { - ExPolygon &expoly = out.emplace_back(); - expoly.contour.points = c2_to_points(outer.Polygon()); - expoly.holes.reserve(outer.Count()); - for (const auto &hole : outer) - expoly.holes.emplace_back().points = c2_to_points(hole->Polygon()); - // Islands inside the holes. - for (const auto &hole : outer) - for (const auto &island : *hole) - c2_append_expolygons(*island, out); + if (C2::Area(outer.Polygon()) < 0.) { + stray_holes.emplace_back(&outer.Polygon()); + for (const auto &island : outer) + c2_append_expolygons(*island, out, stray_holes); + return; + } + // Indexed, as the recursion below may reallocate out. + const size_t idx = out.size(); + out.emplace_back().contour.points = c2_to_points(outer.Polygon()); + out[idx].holes.reserve(outer.Count()); + for (const auto &child : outer) + if (C2::Area(child->Polygon()) > 0.) + c2_append_expolygons(*child, out, stray_holes); + else { + out[idx].holes.emplace_back().points = c2_to_points(child->Polygon()); + // Islands inside the hole. + for (const auto &island : *child) + c2_append_expolygons(*island, out, stray_holes); + } +} + +// Whether outer encloses inner, two paths of one Clipper2 result. Such paths don't cross, they only touch, and rounding +// both to the grid leaves a touching vertex up to about 1.5 units across the other path. So a vertex of inner that close +// to outer doesn't count, the first vertex clear of it decides, and a path touching at every vertex goes by a point +// strictly inside it: either lies on the same side of outer as the whole path. A sliver too thin to hold such a point is +// taken as not enclosed. A hole closely hugging an island inside it would pass, so callers skip any outer no larger than +// inner: a path inside another is always the smaller. +bool path_encloses(const Polygon &outer, const Polygon &inner) +{ + // The rounding of the two paths, and of the projection Polygon::on_boundary() measures to. + static constexpr double touching = 3.; + for (const Point &pt : inner.points) + if (! outer.on_boundary(pt, touching)) + return contains(outer, pt, true); + const std::optional pt = ClipperUtils::interior_point(inner); + return pt && contains(outer, *pt, true); +} + +// Gives each stray hole to the innermost of the contours out[first..] around it. Contours of a Clipper2 result don't +// cross, so those around a hole are nested and the innermost one is the smallest: candidates are tried smallest first, +// from the first one larger than the hole. +void c2_attach_stray_holes(ExPolygons &out, size_t first, const std::vector &stray_holes) +{ + if (stray_holes.empty()) + return; + using Tree = AABBTreeIndirect::Tree<2, coord_t>; + std::vector bboxes; + bboxes.reserve(out.size() - first); + for (size_t i = first; i < out.size(); ++ i) + bboxes.emplace_back(i, get_extents(out[i].contour)); + Tree tree; + tree.build_modify_input(bboxes); + // Contour areas, computed when a contour first becomes a candidate. + std::vector areas(out.size(), 0.); + std::vector candidates; + for (const C2::Path64 *path : stray_holes) { + Polygon hole; + hole.points = c2_to_points(*path); + const double hole_area = - C2::Area(*path); + const BoundingBox bbox = get_extents(hole); + const Tree::BoundingBox hole_bbox(bbox.min, bbox.max); + candidates.clear(); + AABBTreeIndirect::traverse(tree, + [&hole_bbox](const Tree::Node &node) { return node.bbox.contains(hole_bbox); }, + [&candidates](const Tree::Node &node) { candidates.emplace_back(node.idx); return true; }); + for (size_t idx : candidates) + if (areas[idx] == 0.) + areas[idx] = out[idx].contour.area(); + std::sort(candidates.begin(), candidates.end(), [&areas](size_t a, size_t b) { return areas[a] < areas[b]; }); + auto larger = std::upper_bound(candidates.begin(), candidates.end(), hole_area, [&areas](double area, size_t idx) { return area < areas[idx]; }); + if (auto it = std::find_if(larger, candidates.end(), [&out, &hole](size_t idx) { return path_encloses(out[idx].contour, hole); }); + it == candidates.end()) + // Nothing around it: keep the path as Clipper2 returned it. + out.emplace_back().contour = std::move(hole); + else + out[*it].holes.emplace_back(std::move(hole)); + } } ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree) @@ -305,8 +383,10 @@ ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree) cnt += c2_count_expolygons(*outer); ExPolygons out; out.reserve(cnt); + std::vector stray_holes; for (const auto &outer : tree) - c2_append_expolygons(*outer, out); + c2_append_expolygons(*outer, out, stray_holes); + c2_attach_stray_holes(out, 0, stray_holes); return out; } @@ -800,6 +880,114 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + +namespace ClipperUtils { + std::vector tile_expolygons(const ExPolygons &expolygons, size_t per_tile) + { + BoundingBox extent; + std::vector bboxes; + bboxes.reserve(expolygons.size()); + for (const ExPolygon &expoly : expolygons) { + bboxes.emplace_back(get_extents(expoly)); + extent.merge(bboxes.back()); + } + if (! extent.defined) + return {}; + const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max(per_tile, 1)))), 1, 32); + const Point size = extent.size(); + const coord_t tile_w = std::max(1, size.x() / tiles + 1), tile_h = std::max(1, size.y() / tiles + 1); + std::vector out(size_t(tiles * tiles)); + for (size_t i = 0; i < expolygons.size(); ++ i) { + const Point c = bboxes[i].center(); + ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles + + std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))]; + tile.members.emplace_back(i); + tile.bbox.merge(bboxes[i]); + } + out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end()); + return out; + } + + // Between two consecutive heights of its vertices the polygon is a set of trapezoids, and the line through the middle + // of that band meets no vertex and no horizontal edge: its crossings with the edges alternate into and out of the + // polygon, and there each trapezoid is at least half as wide as at its widest. So the bands are tried widest first, + // until the middle of the widest span on that line rounds to a point strictly inside. A band under two units has no + // grid line inside it. + std::optional interior_point(const Polygon &polygon) + { + std::vector heights; + heights.reserve(polygon.size()); + for (const Point &pt : polygon.points) + heights.emplace_back(pt.y()); + std::sort(heights.begin(), heights.end()); + heights.erase(std::unique(heights.begin(), heights.end()), heights.end()); + std::vector> bands; + for (size_t i = 1; i < heights.size(); ++ i) + if (heights[i] - heights[i - 1] >= 2) + bands.emplace_back(heights[i - 1], heights[i]); + std::sort(bands.begin(), bands.end(), [](const auto &a, const auto &b) { return a.second - a.first > b.second - b.first; }); + std::vector crossings; + for (const auto &[low, high] : bands) { + const coord_t y = low + (high - low) / 2; + crossings.clear(); + for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i ++) { + const Point &a = polygon.points[j]; + const Point &b = polygon.points[i]; + if ((a.y() > y) != (b.y() > y)) + crossings.emplace_back(double(a.x()) + double(y - a.y()) * double(b.x() - a.x()) / double(b.y() - a.y())); + } + std::sort(crossings.begin(), crossings.end()); + // Inside from the first crossing to the second, from the third to the fourth... + double left = 0., right = 0.; + for (size_t i = 1; i < crossings.size(); i += 2) + if (crossings[i] - crossings[i - 1] > right - left) { + left = crossings[i - 1]; + right = crossings[i]; + } + if (const Point pt(coord_t(std::round(0.5 * (left + right))), y); right > left && contains(polygon, pt, false)) + return pt; + } + return std::nullopt; + } +} + +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 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) + clip_bboxes.emplace_back(get_extents(polygon)); + + std::vector out_tiles(tiles.size()); + tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) { + const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx]; + Slic3r::ExPolygons local_subject; + local_subject.reserve(tile.members.size()); + for (size_t i : tile.members) + local_subject.emplace_back(subject[i]); + // Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset. + const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON); + Polygons local_clip; + for (size_t i = 0; i < clip.size(); ++i) + if (clip_bboxes[i].overlap(bbox)) + if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty()) + local_clip.emplace_back(std::move(clipped)); + out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset); + }); + Slic3r::ExPolygons out; + for (Slic3r::ExPolygons &out_tile : out_tiles) + append(out, std::move(out_tile)); + return out; +} +Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) + { return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); } +Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) + { return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); } // May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative). Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type) { return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); } @@ -1007,24 +1195,75 @@ Lines _clipper_ln(ClipType clipType, const Lines &subject, const Polygons &clip) return retval; } -static void traverse_pt_outside_in(const C2::PolyPath64 &parent, Polygons *retval) +// Chains the loops of one level of the tree, each followed by the loops under it, depth first. Clipper2 may put a loop +// beside the loop around it instead of under it (see c2_append_expolygons()), out of the nesting order the callers rely +// on: the spiral of FillSpiralInset continues only into a loop inside the last one. So a loop whose orientation doesn't +// fit its level (CCW under the top or a CW loop, CW under a CCW loop) goes under the smallest loop of the level around +// it, the one Clipper2 failed to put it under, and stays where it is if none is. +static void traverse_pt_outside_in(const std::vector &nodes, bool holes, Polygons *retval) { - // collect ordering points - Points ordering_points; - ordering_points.reserve(parent.Count()); - for (const auto &node : parent) - ordering_points.emplace_back(node->Polygon().front().x, node->Polygon().front().y); + struct Loop + { + const C2::PolyPath64 *node; + double area; + // The loops to traverse under this one: its children, then the misplaced loops it takes in. + std::vector next; + bool moved = false; + }; + std::vector loops; + std::vector fitting, misfits; + loops.reserve(nodes.size()); + for (const C2::PolyPath64 *node : nodes) { + Loop &loop = loops.emplace_back(Loop{ node, C2::Area(node->Polygon()), {} }); + loop.next.reserve(node->Count()); + for (const auto &child : *node) + loop.next.emplace_back(child.get()); + ((loop.area < 0.) == holes ? fitting : misfits).emplace_back(loops.size() - 1); + } + if (! misfits.empty()) { + // Candidates are tried smallest first, from the first one larger than the misplaced loop (see path_encloses()). + std::sort(fitting.begin(), fitting.end(), [&loops](size_t a, size_t b) { return std::abs(loops[a].area) < std::abs(loops[b].area); }); + std::vector bounds(loops.size()); + for (size_t i = 0; i < loops.size(); ++ i) + bounds[i] = C2::GetBounds(loops[i].node->Polygon()); + for (size_t misfit : misfits) { + Loop &loop = loops[misfit]; + const C2::Rect64 &box = bounds[misfit]; + Polygon inner; + inner.points = c2_to_points(loop.node->Polygon()); + for (size_t idx : fitting) { + // The slack of a vertex rounded across the candidate, as in c2_attach_stray_holes(). + const C2::Rect64 &around = bounds[idx]; + if (std::abs(loops[idx].area) <= std::abs(loop.area) || around.left - SCALED_EPSILON > box.left || + around.right + SCALED_EPSILON < box.right || around.top - SCALED_EPSILON > box.top || around.bottom + SCALED_EPSILON < box.bottom) + continue; + Polygon outer; + outer.points = c2_to_points(loops[idx].node->Polygon()); + if (path_encloses(outer, inner)) { + loops[idx].next.emplace_back(loop.node); + loop.moved = true; + break; + } + } + } + } - // Perform the ordering, push results recursively. + // Chain the loops staying on this level, push results recursively. + std::vector staying; + Points ordering_points; + for (size_t i = 0; i < loops.size(); ++ i) + if (! loops[i].moved) { + staying.emplace_back(i); + ordering_points.emplace_back(loops[i].node->Polygon().front().x, loops[i].node->Polygon().front().y); + } //FIXME pass the last point to chain_points? for (size_t idx : chain_points(ordering_points)) { - const C2::PolyPath64 &node = *parent.Child(idx); - retval->emplace_back().points = c2_to_points(node.Polygon()); - if (node.IsHole()) + const Loop &loop = loops[staying[idx]]; + retval->emplace_back().points = c2_to_points(loop.node->Polygon()); + if (loop.area < 0.) // Orient a hole, which is clockwise oriented, to CCW. retval->back().reverse(); - // traverse the next depth - traverse_pt_outside_in(node, retval); + traverse_pt_outside_in(loop.next, loop.area > 0., retval); } } @@ -1032,8 +1271,12 @@ Polygons union_pt_chained_outside_in(const Polygons &subject) { C2::PolyTree64 tree; c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, C2::FillRule::EvenOdd, tree); + std::vector top_level; + top_level.reserve(tree.Count()); + for (const auto &node : tree) + top_level.emplace_back(node.get()); Polygons retval; - traverse_pt_outside_in(tree, &retval); + traverse_pt_outside_in(top_level, false, &retval); return retval; } @@ -1070,6 +1313,8 @@ ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::ExPolygonsProvider(expolygons)), {}, C2::FillRule::EvenOdd, tree); ExPolygons out; out.reserve(tree.Count()); + const size_t first_nested = nested ? nested->size() : 0; + std::vector stray_holes; for (const auto &outer : tree) { ExPolygon &expoly = out.emplace_back(); expoly.contour.points = c2_to_points(outer->Polygon()); @@ -1077,9 +1322,11 @@ ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested expoly.holes.emplace_back().points = c2_to_points(hole->Polygon()); if (nested) for (const auto &island : *hole) - c2_append_expolygons(*island, *nested); + c2_append_expolygons(*island, *nested, stray_holes); } } + if (nested) + c2_attach_stray_holes(*nested, first_nested, stray_holes); return out; } diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 26585a1fef..43904ff4b4 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -5,12 +5,14 @@ #include "Polyline.hpp" #include "Line.hpp" #include "libslic3r.h" +#include "BoundingBox.hpp" #include "ExPolygon.hpp" #include "Polygon.hpp" #include "Surface.hpp" #include #include #include +#include #include namespace Slic3r { @@ -332,6 +334,19 @@ 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 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 + { + BoundingBox bbox; + std::vector members; + }; + [[nodiscard]] std::vector tile_expolygons(const ExPolygons &expolygons, size_t per_tile); + + // A grid point strictly inside the polygon, or none if the polygon is at most a few units thick everywhere. + // Decides on which side of another path a path lies when every one of its vertices touches that path. + [[nodiscard]] std::optional interior_point(const Polygon &polygon); + } // offset Polygons @@ -527,6 +542,11 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); 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: 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); Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip); Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip); diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 1e6eade821..d2d6bfb0f5 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -22,6 +22,8 @@ #include "../PrintConfig.hpp" #include "../Surface.hpp" +#include + #include "AABBTreeLines.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/ExPolygon.hpp" @@ -675,24 +677,28 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n if (!line_based_pattern) { const coord_t scaled_spacing = scaled(fill.params.spacing); - for (const ExPolygon &expolygon : fill.expolygons) { + // Each expolygon is split on its own, so they run in parallel and are collected in their original order. + std::vector> split_parts(fill.expolygons.size()); // normal, narrow + tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) { + const ExPolygon &expolygon = fill.expolygons[idx]; Polygons filled_area = to_polygons(expolygon); // "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon. Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing)); if (inner_area.empty()) { - narrow_infill.emplace_back(expolygon); - continue; + split_parts[idx].second.emplace_back(expolygon); + return; } ExPolygons inner_ex = union_ex(inner_area); ExPolygons expolys{expolygon}; - ExPolygons narrow_ex = diff_ex(expolys, inner_ex); - ExPolygons normal_ex = intersection_ex(expolys, inner_ex); - - append(normal_infill, normal_ex); // normal infill area - append(narrow_infill, narrow_ex); // narrow infill area + split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area + split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area + }); + for (auto &[normal_ex, narrow_ex] : split_parts) { + append(normal_infill, std::move(normal_ex)); + append(narrow_infill, std::move(narrow_ex)); } return; @@ -714,7 +720,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n } const double aligning_angle = -base_angle + PI; - for (const ExPolygon &expolygon : fill.expolygons) { + // Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order. + std::vector split_reconstructed(fill.expolygons.size()); + tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) { + const ExPolygon &expolygon = fill.expolygons[expolygon_idx]; Polygons filled_area = to_polygons(expolygon); polygons_rotate(filled_area, aligning_angle); BoundingBox bb = get_extents(filled_area); @@ -845,8 +854,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n } } - polygons_append(normal_fill_areas, reconstructed_area); - } + split_reconstructed[expolygon_idx] = std::move(reconstructed_area); + }); + for (Polygons &reconstructed_area : split_reconstructed) + polygons_append(normal_fill_areas, std::move(reconstructed_area)); polygons_rotate(normal_fill_areas, -aligning_angle); @@ -1473,7 +1484,15 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox); f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr; - f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); + // Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the + // expolygon's box first (padded past the safety offset, which grows the clip side). The result is + // identical; the cost is not: a layer split into many small fills, e.g. by colour painting, + // otherwise intersects every one of them with the whole layer. + BoundingBox no_overlap_bbox = get_extents(expoly); + no_overlap_bbox.offset(SCALED_EPSILON); + f->no_overlap_expolygons = intersection_ex( + ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox), + ExPolygons() = {expoly}, ApplySafetyOffset::Yes); if (params.symmetric_infill_y_axis) { params.symmetric_y_axis = f->extended_object_bounding_box().center().x(); expoly.symmetric_y(params.symmetric_y_axis); diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp index 98b8dfa37c..4ac7ce6cb6 100644 --- a/src/libslic3r/GCode/OrderingStrategies.cpp +++ b/src/libslic3r/GCode/OrderingStrategies.cpp @@ -6,6 +6,7 @@ #include "../ShortestPath.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/libslic3r.h" #include #include @@ -141,15 +142,87 @@ bool tsp_remove_crossings(std::vector& path, const Points& centers) return {std::numeric_limits::max(), std::numeric_limits::max()}; }; + // For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a + // cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan + // (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and + // runs again after every reversal; rebuilding the grid costs more than it saves below the threshold. + constexpr size_t grid_min_size = 500; + BoundingBox extent; + for (size_t idx : path) + extent.merge(centers[idx]); + const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256); + const coord_t cell_w = std::max(1, (extent.max.x() - extent.min.x()) / grid_n + 1); + const coord_t cell_h = std::max(1, (extent.max.y() - extent.min.y()) / grid_n + 1); + const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) { + const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w); + const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h); + for (int y = y0; y <= y1; ++y) + for (int x = x0; x <= x1; ++x) + fn(y * grid_n + x); + }; + std::vector> edge_cells(size_t(grid_n) * grid_n); + + auto find_crossing_grid = [&]() -> std::pair { + for (std::vector& cell : edge_cells) + cell.clear(); + for (size_t j = 0; j < n_edges; ++j) + for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); }); + + for (size_t i = 0; i < n_edges; ++i) { + const Point& ai = centers[path[i]]; + const Point& bi = centers[path[(i + 1) % pn]]; + + size_t first_j = std::numeric_limits::max(); + for_cells(ai, bi, [&](int cell) { + for (size_t j : edge_cells[cell]) { + if (j < i + 2 || j >= first_j) continue; + // Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0. + if (i == 0 && j == pn - 1) continue; + + const Point& aj = centers[path[j]]; + const Point& bj = centers[path[(j + 1) % pn]]; + + if (!bboxes_overlap(ai, bi, aj, bj)) continue; + if (Geometry::segments_intersect(ai, bi, aj, bj)) + first_j = j; + } + }); + if (first_j != std::numeric_limits::max()) + return {i, first_j}; + } + return {std::numeric_limits::max(), std::numeric_limits::max()}; + }; + // 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; - while (max_iters-- > 0) { - auto [ci, cj] = find_crossing(); - if (ci == std::numeric_limits::max()) break; - improved = true; + // 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. 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; + }; + const auto reverse_first_crossing = [&]() { + auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing(); + if (ci == std::numeric_limits::max()) + return false; std::reverse(path.begin() + ci + 1, path.begin() + cj + 1); + 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/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 932f8fb99e..c318f4f5d7 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -1207,21 +1208,21 @@ std::optional> SeamPlacer::find_next_seam_in_layer( const size_t layer_idx, const float max_distance, const SeamPlacerImpl::SeamComparator &comparator) const { using namespace SeamPlacerImpl; - std::vector nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position, - max_distance); - - if (nearby_points_indices.empty()) { - return {}; - } - - size_t best_nearby_point_index = nearby_points_indices[0]; - size_t nearest_point_index = nearby_points_indices[0]; - - // Now find best nearby point, nearest point, and corresponding indices - for (const size_t &nearby_point_index : nearby_points_indices) { + // Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within + // the radius, so they are looked at as the search finds them rather than collected into a vector first. + constexpr size_t none = std::numeric_limits::max(); + size_t best_nearby_point_index = none; + size_t nearest_point_index = none; + visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance, + [&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index] + (size_t nearby_point_index) { + if (best_nearby_point_index == none) { + // The first point found starts both, as the first of the collected ones did. + best_nearby_point_index = nearest_point_index = nearby_point_index; + } const SeamCandidate &point = layers[layer_idx].points[nearby_point_index]; if (point.perimeter.finalized) { - continue; // skip over finalized perimeters, try to find some that is not finalized + return; // skip over finalized perimeters, try to find some that is not finalized } if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index], projected_position.head<2>()) @@ -1233,6 +1234,10 @@ std::optional> SeamPlacer::find_next_seam_in_layer( || layers[layer_idx].points[nearest_point_index].perimeter.finalized) { nearest_point_index = nearby_point_index; } + }); + + if (best_nearby_point_index == none) { + return {}; } const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index]; diff --git a/src/libslic3r/KDTreeIndirect.hpp b/src/libslic3r/KDTreeIndirect.hpp index d334a2ad64..3bf3d938e6 100644 --- a/src/libslic3r/KDTreeIndirect.hpp +++ b/src/libslic3r/KDTreeIndirect.hpp @@ -318,6 +318,36 @@ std::vector find_nearby_points(const KDTreeIndirectType &kdtree, const P return visitor.result; } +// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands +// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the +// points into a vector than on the search itself, and its caller usually keeps only a few of them. +template +void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType ¢er, + const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn) +{ + using CoordType = typename KDTreeIndirectType::CoordType; + + struct Visitor { + const KDTreeIndirectType &kdtree; + const PointType center; + const CoordType max_distance_squared; + VisitorFn visitor_fn; + + unsigned int operator()(size_t idx, size_t dimension) { + auto dist = CoordType(0); + for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) { + CoordType d = center[i] - kdtree.coordinate(idx, i); + dist += d * d; + } + if (dist < max_distance_squared) + visitor_fn(idx); + return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension); + } + } visitor { kdtree, center, max_distance * max_distance, visitor_fn }; + + kdtree.visit(visitor); +} + template std::vector find_nearby_points(const KDTreeIndirectType &kdtree, const PointType ¢er, const typename KDTreeIndirectType::CoordType& max_distance) diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index c20cebc641..eef3f90c87 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -99,10 +99,11 @@ void LayerRegion::slices_to_fill_surfaces_clipped() by_surface[size_t(surface.surface_type)].emplace_back(&surface); // Trim surfaces by the fill_boundaries. this->fill_surfaces.surfaces.clear(); + const Polygons fill_boundaries = to_polygons(this->fill_expolygons); for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) { const SurfacesPtr &this_surfaces = by_surface[surface_type]; if (! this_surfaces.empty()) - this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type)); + this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type)); } } diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index dc3af43de4..507d997f1d 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -22,6 +22,7 @@ #include "Surface.hpp" #include "libslic3r.h" +#include #include #include #include @@ -1361,10 +1362,15 @@ static inline std::vector> segmentation_top_and_bottom_l } #endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM - // When the upper surface of an object is occluded, it should no longer be considered the upper surface + // When the upper surface of an object is occluded, it should no longer be considered the upper surface. + // Every (colour, layer) pair is trimmed on its own, so they all run at once: the painted faces of a finely + // textured part project hundreds of thousands of triangles onto one layer, which used to be trimmed serially. { - for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) { - for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { + const size_t occluded_pairs = num_facets_states * layers.size(); + tbb::parallel_for(tbb::blocked_range(0, occluded_pairs), [&](const tbb::blocked_range &range) { + for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) { + const size_t extruder_idx = pair_idx / layers.size(); + const size_t layer_idx = pair_idx % layers.size(); if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) { top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]); } @@ -1372,7 +1378,7 @@ static inline std::vector> segmentation_top_and_bottom_l bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]); } } - } + }); } std::vector> triangles_by_color_bottom(num_facets_states); @@ -1430,11 +1436,58 @@ static inline std::vector> segmentation_top_and_bottom_l return out; }; + // Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in + // `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 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) { + std::vector offsets(shell_layers.size()); + float offset = 0.f; + for (size_t i = 0; i < shell_layers.size(); ++i) { + //BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line + offset -= (stat.extrusion_spacing + stat.extrusion_width); + offsets[i] = offset; + } + if (offsets.empty()) + return; + const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON; + const std::vector tiles = ClipperUtils::tile_expolygons(ex, 16); + // [shell layer][tile] + std::vector> shells(shell_layers.size(), std::vector(tiles.size())); + tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) { + const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx]; + const BoundingBox bbox = tile.bbox.inflated(reach); + ExPolygons tile_ex; + tile_ex.reserve(tile.members.size()); + for (size_t i : tile.members) + tile_ex.emplace_back(ex[i]); + Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox); + for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) { + const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox)); + shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold); + layer_slices_trimmed = to_polygons(trimmed); + } + }); + for (size_t i = 0; i < shell_layers.size(); ++i) { + ExPolygons shell; + for (ExPolygons &tile_shell : shells[i]) + append(shell, std::move(tile_shell)); + if (shell.empty()) + break; + dst.emplace_back(shell_layers[i], std::move(shell)); + } + }; + tbb::parallel_for(tbb::blocked_range(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top, - &throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom, + &throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells, &shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { - for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) { + // Each colour writes only its own vectors, so the colours run in parallel: a painted top or bottom face + // projects onto a single layer, which otherwise did all of its colours on one thread. + tbb::parallel_for(size_t(0), size_t(num_facets_states), [&](size_t color_idx) { throw_on_cancel_callback(); LayerColorStat stat = layer_color_stat(layer_idx, color_idx); if (std::vector &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty()) @@ -1443,18 +1496,10 @@ static inline std::vector> segmentation_top_and_bottom_l top_ex = opening_ex(top_ex, stat.small_region_threshold); if (! top_ex.empty()) { append(triangles_by_color_top[color_idx][layer_idx], top_ex); - float offset = 0.f; - ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; - for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) { - //BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line - //offset -= stat.extrusion_width ; - offset -= (stat.extrusion_spacing + stat.extrusion_width); - layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]); - ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold); - if (last.empty()) - break; - shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last)); - } + std::vector shell_layers; + for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) + shell_layers.emplace_back(size_t(last_idx)); + project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]); } } if (std::vector &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty()) @@ -1463,21 +1508,13 @@ static inline std::vector> segmentation_top_and_bottom_l bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold); if (! bottom_ex.empty()) { append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex); - float offset = 0.f; - ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; - for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) { - //BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line - //offset -= stat.extrusion_width; - offset -= (stat.extrusion_spacing + stat.extrusion_width); - layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]); - ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold); - if (last.empty()) - break; - shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last)); - } + std::vector shell_layers; + for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) + shell_layers.emplace_back(last_idx); + project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]); } } - } + }); } }); @@ -1498,20 +1535,23 @@ static inline std::vector> segmentation_top_and_bottom_l &shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { throw_on_cancel_callback(); - ExPolygons painted_exploys; - for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { + // The per-colour unions below are independent of each other, so they run in parallel (a painted top or + // bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order. + const auto merge_colour_union = [&](size_t color_idx) { auto &self = triangles_by_color_merged[color_idx][layer_idx]; append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx])); append(self, std::move(triangles_by_color_top[color_idx][layer_idx])); self = union_ex(self); + }; + tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union); - append(painted_exploys, self); - } - + ExPolygons painted_exploys; + for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) + append(painted_exploys, triangles_by_color_merged[color_idx][layer_idx]); painted_exploys = union_ex(painted_exploys); //BBS: merge the top and bottom shell layers - for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { + tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) { auto &self = triangles_by_color_merged[color_idx][layer_idx]; auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys); @@ -1520,7 +1560,7 @@ static inline std::vector> segmentation_top_and_bottom_l append(self, top_area); append(self, bottom_area); self = union_ex(self); - } + }); // Trim one region by the other if some of the regions overlap. ExPolygons painted_regions; for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) { @@ -1887,7 +1927,69 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto } } -static std::vector> merge_segmented_layers(const std::vector> &segmented_regions, + +// Finds the islands (layer ExPolygons) a region piece overlaps. A top or bottom region is projected from the neighbouring +// layers and may reach past the island it belongs to, or over several islands. +class IslandLocator +{ +public: + explicit IslandLocator(const ExPolygons &islands) : m_islands(islands) + { + m_bboxes.reserve(islands.size()); + for (const ExPolygon &island : islands) { + m_bboxes.emplace_back(get_extents(island)); + m_extent.merge(m_bboxes.back()); + } + if (!m_extent.defined) + return; + const Point size = m_extent.size(); + m_cell_w = std::max(1, size.x() / GRID + 1); + m_cell_h = std::max(1, size.y() / GRID + 1); + m_grid.assign(GRID * GRID, {}); + for (size_t i = 0; i < m_bboxes.size(); ++i) + for_cells(m_bboxes[i], [&](int cell) { m_grid[cell].emplace_back(i); }); + } + + void find(const ExPolygon &piece, std::vector &out) const + { + out.clear(); + const BoundingBox bbox = get_extents(piece); + if (!m_extent.defined || !m_extent.overlap(bbox)) + return; + for_cells(bbox, [&](int cell) { + for (size_t i : m_grid[cell]) + if (m_bboxes[i].overlap(bbox)) + out.emplace_back(i); + }); + sort_remove_duplicates(out); + if (out.size() > 1) + out.erase(std::remove_if(out.begin(), out.end(), [&](size_t i) { + const BoundingBox common(m_bboxes[i].min.cwiseMax(bbox.min), m_bboxes[i].max.cwiseMin(bbox.max)); + return intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(piece, common.inflated(SCALED_EPSILON)), + ClipperUtils::clip_clipper_polygons_with_subject_bbox(m_islands[i], common.inflated(SCALED_EPSILON))).empty(); + }), out.end()); + } + +private: + static constexpr int GRID = 64; + template void for_cells(const BoundingBox &bb, Fn &&fn) const + { + const int x0 = std::clamp(int((bb.min.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1); + const int y0 = std::clamp(int((bb.min.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1); + for (int y = y0; y <= y1; ++y) + for (int x = x0; x <= x1; ++x) + fn(y * GRID + x); + } + + const ExPolygons &m_islands; + std::vector m_bboxes; + BoundingBox m_extent; + coord_t m_cell_w = 1, m_cell_h = 1; + std::vector> m_grid; +}; + +static std::vector> merge_segmented_layers(const std::vector &input_expolygons, + const std::vector> &segmented_regions, std::vector> &&top_and_bottom_layers, const size_t num_facets_states, const std::function &throw_on_cancel_callback) @@ -1898,33 +2000,91 @@ static std::vector> merge_segmented_layers(const std::ve assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size()); BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin"; - tbb::parallel_for(tbb::blocked_range(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range &range) { + // 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 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) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { assert(segmented_regions[layer_idx].size() == num_facets_states); - // Zero is skipped because it is the default color of the volume + throw_on_cancel_callback(); + // Group the islands joined by a region overlapping several of them; the last group takes the regions lying + // outside every island. + const ExPolygons &islands = input_expolygons[layer_idx]; + const IslandLocator locator(islands); + std::vector parent(islands.size() + 1); + std::iota(parent.begin(), parent.end(), 0); + const auto root = [&parent](size_t i) { + while (parent[i] != i) + i = parent[i] = parent[parent[i]]; + return i; + }; + // Islands of every piece: side regions of colours 1.., then top/bottom regions of colours 0.. + std::vector pieces; + for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) + for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id]) + pieces.emplace_back(&piece); + if (!top_and_bottom_layers.empty()) + for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) + for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx]) + pieces.emplace_back(&piece); + std::vector> overlapped(pieces.size()); + tbb::parallel_for(size_t(0), pieces.size(), [&](size_t i) { locator.find(*pieces[i], overlapped[i]); }); + std::vector piece_island(pieces.size()); + for (size_t i = 0; i < pieces.size(); ++i) { + piece_island[i] = overlapped[i].empty() ? islands.size() : overlapped[i].front(); + for (size_t island : overlapped[i]) + parent[root(island)] = root(piece_island[i]); + } + std::vector bucket_of(parent.size(), size_t(-1)); + size_t num_buckets = 0; + for (size_t i = 0; i < parent.size(); ++i) + if (size_t &b = bucket_of[root(i)]; b == size_t(-1)) + b = num_buckets++; + + // [bucket][colour] + std::vector> sides(num_buckets, std::vector(num_facets_states)); + std::vector> tops(num_buckets, std::vector(num_facets_states)); + size_t piece_idx = 0; + for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) + for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id]) + sides[bucket_of[root(piece_island[piece_idx++])]][extruder_id].emplace_back(piece); + if (!top_and_bottom_layers.empty()) + for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) + for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx]) + tops[bucket_of[root(piece_island[piece_idx++])]][color_idx].emplace_back(piece); + + // Side regions minus the top/bottom regions of every colour. + std::vector> merged(num_buckets, std::vector(num_facets_states)); + tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) { + Polygons tops_all; + for (const ExPolygons &t : tops[bucket]) + polygons_append(tops_all, t); + for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) + if (!sides[bucket][extruder_id].empty()) + merged[bucket][extruder_id] = tops_all.empty() ? std::move(sides[bucket][extruder_id]) : + diff_ex_by_piece(sides[bucket][extruder_id], tops_all); + }); + + // Then this colour's top/bottom regions, with the dimples removed (#7235) when the layer has side regions left. for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) { - throw_on_cancel_callback(); - if (!segmented_regions[layer_idx][extruder_id].empty()) { - ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id]; - if (!top_and_bottom_layers.empty()) { - for (const std::vector &top_and_bottom_by_extruder : top_and_bottom_layers) { - if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) { - segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]); - } - } - } - - segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed); - } - - if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) { - bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty(); - append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]); - - // Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers. - if (!was_top_and_bottom_empty) - segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON)); + if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) { + for (size_t bucket = 0; bucket < num_buckets; ++bucket) + append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id])); + continue; } + bool was_top_and_bottom_empty = true; + for (size_t bucket = 0; bucket < num_buckets && was_top_and_bottom_empty; ++bucket) + was_top_and_bottom_empty = merged[bucket][extruder_id].empty(); + tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) { + ExPolygons ®ion = merged[bucket][extruder_id]; + append(region, tops[bucket][extruder_id]); + if (!was_top_and_bottom_empty && !region.empty()) + region = offset2_ex(union_ex(region), float(SCALED_EPSILON), -float(SCALED_EPSILON)); + }); + for (size_t bucket = 0; bucket < num_buckets; ++bucket) + append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id])); } } }); // end of parallel_for @@ -2212,16 +2372,56 @@ std::vector> segmentation_by_painting(const PrintObject assert(!color_poly.empty()); assert(!color_poly.front().empty()); - if (has_layer_only_one_color(color_poly)) { - // If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer. - segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx]; - } else { - MMU_Graph graph = build_graph(layer_idx, color_poly); - remove_multiple_edges_in_vertices(graph, color_poly); - graph.remove_nodes_with_one_arc(); - segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states); - //segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx); + // Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to + // that island's contours than to any other island's - the way out crosses its own boundary first - so its + // Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief + // has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions + // that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in + // parallel; an island in a single colour needs no diagram at all. + const ExPolygons &islands = input_expolygons[layer_idx]; + std::vector> island_contours(islands.size()); // [first, last) into color_poly + { + // The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours(). + size_t idx = 0; + for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) { + const size_t first = idx; + if (!islands[island_idx].contour.empty()) + ++idx; + for (const Polygon &hole : islands[island_idx].holes) + if (!hole.empty()) + ++idx; + island_contours[island_idx] = {first, idx}; + } + assert(idx == color_poly.size()); } + std::vector> island_regions(islands.size()); + tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) { + const auto [first, last] = island_contours[island_idx]; + if (first == last) + return; + const std::vector island_poly(color_poly.begin() + first, color_poly.begin() + last); + std::vector ®ions = island_regions[island_idx]; + if (has_layer_only_one_color(island_poly)) { + regions.assign(num_facets_states, ExPolygons()); + regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]); + } else { + MMU_Graph graph = build_graph(layer_idx, island_poly); + remove_multiple_edges_in_vertices(graph, island_poly); + graph.remove_nodes_with_one_arc(); + regions = extract_colored_segments(graph, num_facets_states); + // The faces of one colour tile it without overlapping; merged here, where an island is small, + // every later boolean gets a few regions instead of thousands of faces sharing their edges. An + // island with many holes keeps its faces: merged, each colour would be one region with thousands + // of holes, and subtracting from that is far slower than from the faces one at a time. + if (island_poly.size() <= 64) + for (ExPolygons &faces : regions) + if (faces.size() > 1) + faces = union_ex(faces); + } + }); + for (std::vector ®ions : island_regions) + for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx) + append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx])); #ifdef MM_SEGMENTATION_DEBUG_REGIONS export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]); @@ -2244,7 +2444,7 @@ std::vector> segmentation_by_painting(const PrintObject throw_on_cancel_callback(); } - std::vector> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback); + std::vector> segmented_regions_merged = merge_segmented_layers(input_expolygons, segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback); throw_on_cancel_callback(); #ifdef MM_SEGMENTATION_DEBUG_REGIONS diff --git a/src/libslic3r/MultiPoint.hpp b/src/libslic3r/MultiPoint.hpp index 82d6d5884b..772085b3be 100644 --- a/src/libslic3r/MultiPoint.hpp +++ b/src/libslic3r/MultiPoint.hpp @@ -23,11 +23,15 @@ public: MultiPoint() {} MultiPoint(const MultiPoint &other) : points(other.points) {} - MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {} + MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {} MultiPoint(std::initializer_list list) : points(list) {} explicit MultiPoint(const Points &_points) : points(_points) {} + // Without it, the derived classes' move constructors passing std::move(points) here copied them, which + // also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its + // points: a use-after-move anywhere in the tree that happened to work before now sees nothing. + explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {} MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; } - MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; } + MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; } virtual ~MultiPoint() = default; void scale(double factor); void scale(double factor_x, double factor_y); diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index eed1adcf78..219b6b2551 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -32,6 +32,8 @@ #include #include #include +#include +#include #include #include "libslic3r.h" #include @@ -2583,467 +2585,490 @@ void PerimeterGenerator::process_arachne() const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config); // we need to process each island separately because we might have different // extra perimeters for each one - for (const Surface& surface : all_surfaces) { - coord_t bead_width_0 = ext_perimeter_spacing; - // detect how many perimeters must be generated for this island - int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops - int sparse_infill_density = this->config->sparse_infill_density.value; - if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall - loop_number++; + // Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands + // of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the + // original island order, which is what the extra overhang perimeters (applied to the last island's loops and to + // all fill surfaces so far) depend on. + struct ArachneSurfaceResult + { + ExtrusionEntityCollection loops; + bool has_loops = false; + ExPolygons infill; + ExPolygons no_overlap; + }; + std::vector results(all_surfaces.size()); + tbb::parallel_for(tbb::blocked_range(0, all_surfaces.size()), [&](const tbb::blocked_range &range) { + for (size_t surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) { + const Surface &surface = all_surfaces[surface_idx]; + ArachneSurfaceResult &result = results[surface_idx]; + coord_t bead_width_0 = ext_perimeter_spacing; + // detect how many perimeters must be generated for this island + int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops + int sparse_infill_density = this->config->sparse_infill_density.value; + if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall + loop_number++; - // Set the bottommost layer to be one wall - const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false; - if (is_bottom_layer && only_one_wall_first_layer) - loop_number = 0; + // Set the bottommost layer to be one wall + const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false; + if (is_bottom_layer && only_one_wall_first_layer) + loop_number = 0; - // Orca: set the topmost layer to be one wall according to the config - const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false; - if (is_topmost_layer && loop_number > 0 && only_one_wall_top) - loop_number = 0; + // Orca: set the topmost layer to be one wall according to the config + const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false; + if (is_topmost_layer && loop_number > 0 && only_one_wall_top) + loop_number = 0; - auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter; - // Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled. - ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution), - apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing ) - : -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); + auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter; + // Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled. + ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution), + apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing ) + : -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); - Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config); - // Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity. - input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false; + Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config); + // Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity. + input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false; - coord_t wall_0_inset = 0; - if (apply_precise_outer_wall) - wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); + coord_t wall_0_inset = 0; + if (apply_precise_outer_wall) + wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); - //PS: One wall top surface for Arachne - ExPolygons top_expolygons; - // Calculate how many inner loops remain when TopSurfaces is selected. - const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1; + //PS: One wall top surface for Arachne + ExPolygons top_expolygons; + // Calculate how many inner loops remain when TopSurfaces is selected. + const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1; - // Set one perimeter when TopSurfaces is selected. - if (only_one_wall_top && loop_number > 0) - loop_number = 0; + // Set one perimeter when TopSurfaces is selected. + if (only_one_wall_top && loop_number > 0) + loop_number = 0; - Arachne::WallToolPathsParams input_params_tmp = input_params; + Arachne::WallToolPathsParams input_params_tmp = input_params; - Polygons last_p = to_polygons(last); - Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1), - wall_0_inset, layer_height, input_params_tmp); - std::vector perimeters = wallToolPaths.getToolPaths(); - ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour()); + Polygons last_p = to_polygons(last); + Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1), + wall_0_inset, layer_height, input_params_tmp); + std::vector perimeters = wallToolPaths.getToolPaths(); + ExPolygons infill_contour = union_ex(wallToolPaths.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); + // 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); - coord_t perimeter_width = this->perimeter_flow.scaled_width(); + coord_t perimeter_width = this->perimeter_flow.scaled_width(); - // 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); + // 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); - // 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); + // 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); + 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; + ExPolygons top = diff_ex(contour, upper_slices_clipped); + uncovered = !top.empty(); + if (top.empty()) + return top; - 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, contour_bbox); - const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset); + 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, contour_bbox); + const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset); - // Remove bridges from top surface polygons. - top = diff_ex(top, current_slices_bridges); + // Remove bridges from top surface polygons. + top = diff_ex(top, current_slices_bridges); + } + + // 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 = 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). + 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); + } + } } - // 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 = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85)); + bool uncovered = false; + top_expolygons = get_top_expolygons(infill_contour, uncovered); - // Get final top ExPolygons (bridges were excluded above, so they stay walled). - return intersection_ex(top, contour); - }; + 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 + // top-surface islands with inner walls that don't exist when the feature is disabled. + const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config); + const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour + : diff_ex(infill_contour, top_expolygons), + wall_0_inset)); + Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp); + std::vector inner_perimeters = inner_wall_tool_paths.getToolPaths(); - // 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; - }; + if (clip_walls_over_top) { + Polygons kept_over_top; + clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top); + // Route the top fill around the walls kept despite grazing the top. + if (! kept_over_top.empty()) + top_expolygons = diff_ex(top_expolygons, kept_over_top); + } - // 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) { + // Recalculate indexes of inner perimeters before merging them: they come after the single outer wall. + if (!perimeters.empty()) + for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) + 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 use the walls generated like when the single perimeter + // feature is disabled. 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); - } + perimeters = std::move(full_perimeters); + infill_contour = union_ex(full_inner_contour); } } + //PS - bool uncovered = false; - top_expolygons = get_top_expolygons(infill_contour, uncovered); + loop_number = int(perimeters.size()) - 1; - 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 - // top-surface islands with inner walls that don't exist when the feature is disabled. - const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config); - const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour - : diff_ex(infill_contour, top_expolygons), - wall_0_inset)); - Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp); - std::vector inner_perimeters = inner_wall_tool_paths.getToolPaths(); - - if (clip_walls_over_top) { - Polygons kept_over_top; - clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top); - // Route the top fill around the walls kept despite grazing the top. - if (! kept_over_top.empty()) - top_expolygons = diff_ex(top_expolygons, kept_over_top); - } - - // Recalculate indexes of inner perimeters before merging them: they come after the single outer wall. - if (!perimeters.empty()) - for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) - 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 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); + #ifdef ARACHNE_DEBUG + { + static int iRun = 0; + export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); } - } - //PS + #endif - loop_number = int(perimeters.size()) - 1; + // All closed ExtrusionLine should have the same the first and the last point. + // But in rare cases, Arachne produce ExtrusionLine marked as closed but without + // equal the first and the last point. + assert([&perimeters = std::as_const(perimeters)]() -> bool { + for (const Arachne::VariableWidthLines& perimeter : perimeters) + for (const Arachne::ExtrusionLine& el : perimeter) + if (el.is_closed && el.junctions.front().p != el.junctions.back().p) + return false; + return true; + }()); - #ifdef ARACHNE_DEBUG - { - static int iRun = 0; - export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); - } -#endif + int start_perimeter = int(perimeters.size()) - 1; + int end_perimeter = -1; + int direction = -1; - // All closed ExtrusionLine should have the same the first and the last point. - // But in rare cases, Arachne produce ExtrusionLine marked as closed but without - // equal the first and the last point. - assert([&perimeters = std::as_const(perimeters)]() -> bool { - for (const Arachne::VariableWidthLines& perimeter : perimeters) - for (const Arachne::ExtrusionLine& el : perimeter) - if (el.is_closed && el.junctions.front().p != el.junctions.back().p) - return false; - return true; - }()); - - int start_perimeter = int(perimeters.size()) - 1; - int end_perimeter = -1; - int direction = -1; - - bool is_outer_wall_first = - this->config->wall_sequence == WallSequence::OuterInner || - this->config->wall_sequence == WallSequence::InnerOuterInner; + bool is_outer_wall_first = + this->config->wall_sequence == WallSequence::OuterInner || + this->config->wall_sequence == WallSequence::InnerOuterInner; - if (layer_id == 0){ // disable inner outer inner algorithm after the first layer - is_outer_wall_first = - this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim(); - } - if (is_outer_wall_first) { - start_perimeter = 0; - end_perimeter = int(perimeters.size()); - direction = 1; - } - - std::vector all_extrusions; - for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) { - if (perimeters[perimeter_idx].empty()) - continue; - for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx]) - all_extrusions.emplace_back(&wall); - } - - // Find topological order with constraints from extrusions_constrains. - std::vector blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion. - std::vector> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion. - std::unordered_map map_extrusion_to_idx; - for (size_t idx = 0; idx < all_extrusions.size(); idx++) - map_extrusion_to_idx.emplace(all_extrusions[idx], idx); - - auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first); - for (auto [before, after] : extrusions_constrains) { - auto after_it = map_extrusion_to_idx.find(after); - ++blocked[after_it->second]; - blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second); - } - - std::vector processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed. - Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position. - std::vector ordered_extrusions; // To store our result in. At the end we'll std::swap. - ordered_extrusions.reserve(all_extrusions.size()); - - while (ordered_extrusions.size() < all_extrusions.size()) { - size_t best_candidate = 0; - double best_distance_sqr = std::numeric_limits::max(); - bool is_best_closed = false; - - std::vector available_candidates; - for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) { - if (processed[candidate] || blocked[candidate]) - continue; // Not a valid candidate. - available_candidates.push_back(candidate); + if (layer_id == 0){ // disable inner outer inner algorithm after the first layer + is_outer_wall_first = + this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim(); + } + if (is_outer_wall_first) { + start_perimeter = 0; + end_perimeter = int(perimeters.size()); + direction = 1; } - std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool { - return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed; - }); - - for (const size_t candidate_path_idx : available_candidates) { - auto& path = all_extrusions[candidate_path_idx]; - - if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end. - if (best_distance_sqr == std::numeric_limits::max()) { - best_candidate = candidate_path_idx; - is_best_closed = path->is_closed; - } + std::vector all_extrusions; + for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) { + if (perimeters[perimeter_idx].empty()) continue; + for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx]) + all_extrusions.emplace_back(&wall); + } + + // Find topological order with constraints from extrusions_constrains. + std::vector blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion. + std::vector> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion. + std::unordered_map map_extrusion_to_idx; + for (size_t idx = 0; idx < all_extrusions.size(); idx++) + map_extrusion_to_idx.emplace(all_extrusions[idx], idx); + + auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first); + for (auto [before, after] : extrusions_constrains) { + auto after_it = map_extrusion_to_idx.find(after); + ++blocked[after_it->second]; + blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second); + } + + std::vector processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed. + Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position. + std::vector ordered_extrusions; // To store our result in. At the end we'll std::swap. + ordered_extrusions.reserve(all_extrusions.size()); + + while (ordered_extrusions.size() < all_extrusions.size()) { + size_t best_candidate = 0; + double best_distance_sqr = std::numeric_limits::max(); + bool is_best_closed = false; + + std::vector available_candidates; + for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) { + if (processed[candidate] || blocked[candidate]) + continue; // Not a valid candidate. + available_candidates.push_back(candidate); } - const Point candidate_position = path->junctions.front().p; - double distance_sqr = (current_position - candidate_position).cast().norm(); - if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far. - if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits::max()) || (!path->is_closed && !is_best_closed)) { - best_candidate = candidate_path_idx; - best_distance_sqr = distance_sqr; - is_best_closed = path->is_closed; + std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool { + return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed; + }); + + for (const size_t candidate_path_idx : available_candidates) { + auto& path = all_extrusions[candidate_path_idx]; + + if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end. + if (best_distance_sqr == std::numeric_limits::max()) { + best_candidate = candidate_path_idx; + is_best_closed = path->is_closed; + } + continue; } + + const Point candidate_position = path->junctions.front().p; + double distance_sqr = (current_position - candidate_position).cast().norm(); + if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far. + if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits::max()) || (!path->is_closed && !is_best_closed)) { + best_candidate = candidate_path_idx; + best_distance_sqr = distance_sqr; + is_best_closed = path->is_closed; + } + } + } + + auto& best_path = all_extrusions[best_candidate]; + ordered_extrusions.push_back({ best_path, best_path->is_contour() }); + processed[best_candidate] = true; + for (size_t unlocked_idx : blocking[best_candidate]) + blocked[unlocked_idx]--; + + if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then. + if (best_path->is_closed) + current_position = best_path->junctions[0].p; //We end where we started. + else + current_position = best_path->junctions.back().p; //Pick the other end from where we started. } } - auto& best_path = all_extrusions[best_candidate]; - ordered_extrusions.push_back({ best_path, best_path->is_contour() }); - processed[best_candidate] = true; - for (size_t unlocked_idx : blocking[best_candidate]) - blocked[unlocked_idx]--; + // printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer + if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer + if (ordered_extrusions.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. + int arr_i, arr_j = 0; // indexes to run through the walls in the for loops + int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values + + // To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering + // for OI mode that is used the basis for IOI + bringContoursToFront(ordered_extrusions); + std::vector reordered_extrusions; + + // Debug statement to print spacing values: + //printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing()); - if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then. - if (best_path->is_closed) - current_position = best_path->junctions[0].p; //We end where we started. - else - current_position = best_path->junctions.back().p; //Pick the other end from where we started. - } - } - - // printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer - if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer - if (ordered_extrusions.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. - int arr_i, arr_j = 0; // indexes to run through the walls in the for loops - int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values + // Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor + // rounding errors. When precise wall is enabled, the external perimeter full spacing is used. + coord_t threshold_external = (apply_precise_outer_wall) + // Precise outer wall: use the full external spacing + ? ( this->ext_perimeter_flow.scaled_spacing() + + this->perimeter_flow.scaled_spacing()/2.0 ) + // Normal: half ext spacing plus half int spacing + : ( this->ext_perimeter_flow.scaled_spacing()/2.0 + + this->perimeter_flow.scaled_spacing()/2.0 ); - // To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering - // for OI mode that is used the basis for IOI - bringContoursToFront(ordered_extrusions); - std::vector reordered_extrusions; + // For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors. + coord_t threshold_internal = this->perimeter_flow.scaled_spacing(); - // Debug statement to print spacing values: - //printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing()); - - // Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor - // rounding errors. When precise wall is enabled, the external perimeter full spacing is used. - coord_t threshold_external = (apply_precise_outer_wall) - // Precise outer wall ⇒ use “full external spacing” - ? ( this->ext_perimeter_flow.scaled_spacing() - + this->perimeter_flow.scaled_spacing()/2.0 ) - // Normal ⇒ half ext spacing + half int spacing - : ( this->ext_perimeter_flow.scaled_spacing()/2.0 - + this->perimeter_flow.scaled_spacing()/2.0 ); + // Re-order extrusions based on distance + // Alorithm will aggresively optimise for the appearance of the outermost perimeter + ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal ); + reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list. - // For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors. - coord_t threshold_internal = this->perimeter_flow.scaled_spacing(); - - // Re-order extrusions based on distance - // Alorithm will aggresively optimise for the appearance of the outermost perimeter - ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal ); - reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list. - - // Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis - // scan to find the external perimeter, first internal, second internal and last perimeter in the island. - // We then advance the position index to move to the second island and continue until there are no more - // perimeters left. - while (position < reordered_extrusions.size()) { - outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1 - max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list - // run through the walls to get the index values that need re-ordering until the first one for each - // is found. Start at "position" index to enable the for loop to iterate for multiple external - // perimeters in a single island - // printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal); - for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { - // printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i); - switch (reordered_extrusions[arr_i].extrusion->inset_idx) { - case 0: // external perimeter - if (outer == -1) - outer = arr_i; - break; - case 1: // first internal wall - if (first_internal==-1 && arr_i>outer && outer!=-1){ - first_internal = arr_i; - } - break; - case 2: // second internal wall - if (second_internal == -1 && arr_i > first_internal && outer!=-1){ - second_internal = arr_i; - } - break; - } - if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order. - // This means we entered a new island. - arr_i=arr_i-1; //step back one perimeter - max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island. - break; // exit the for loop - } - } - - // printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal); - if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in. - std::vector inner_outer_extrusions; // temporary array to hold extrusions for reordering - inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations - // printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position); - - for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2 - if(arr_j >= second_internal){ - //printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j); - inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j]; - current_perimeter++; + // Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis + // scan to find the external perimeter, first internal, second internal and last perimeter in the island. + // We then advance the position index to move to the second island and continue until there are no more + // perimeters left. + while (position < reordered_extrusions.size()) { + outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1 + max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list + // run through the walls to get the index values that need re-ordering until the first one for each + // is found. Start at "position" index to enable the for loop to iterate for multiple external + // perimeters in a single island + // printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal); + for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { + // printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i); + switch (reordered_extrusions[arr_i].extrusion->inset_idx) { + case 0: // external perimeter + if (outer == -1) + outer = arr_i; + break; + case 1: // first internal wall + if (first_internal==-1 && arr_i>outer && outer!=-1){ + first_internal = arr_i; + } + break; + case 2: // second internal wall + if (second_internal == -1 && arr_i > first_internal && outer!=-1){ + second_internal = arr_i; + } + break; + } + if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order. + // This means we entered a new island. + arr_i=arr_i-1; //step back one perimeter + max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island. + break; // exit the for loop } } + + // printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal); + if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in. + std::vector inner_outer_extrusions; // temporary array to hold extrusions for reordering + inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations + // printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position); - for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order - // printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1); - inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j]; + for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2 + if(arr_j >= second_internal){ + //printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j); + inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j]; + current_perimeter++; + } + } + + for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order + // printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1); + inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j]; + } + + for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array + ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position]; } - - for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array - ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position]; + // go to the next perimeter from the current position to continue scanning for external walls in the same island + position = arr_i + 1; } - // go to the next perimeter from the current position to continue scanning for external walls in the same island - position = arr_i + 1; } } - } - bool steep_overhang_contour = false; - bool steep_overhang_hole = false; - if (!config->overhang_reverse) { - // Skip steep overhang detection no reverse is specified - steep_overhang_contour = true; - steep_overhang_hole = true; - } - if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) { - if (config->overhang_reverse) { - reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, - this->config->overhang_reverse_internal_only); + bool steep_overhang_contour = false; + bool steep_overhang_hole = false; + if (!config->overhang_reverse) { + // Skip steep overhang detection no reverse is specified + steep_overhang_contour = true; + steep_overhang_hole = true; + } + if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) { + if (config->overhang_reverse) { + reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, + this->config->overhang_reverse_internal_only); + } + defer_unsupported_loops(*this, extrusion_coll); + result.loops = std::move(extrusion_coll); + result.has_loops = true; } - defer_unsupported_loops(*this, extrusion_coll); - this->loops->append(extrusion_coll); - } - 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. + if (offset_ex(infill_contour, -float(spacing / 2.)).empty()) + infill_contour.clear(); // Infill region is too small, so let's filter it out. - // create one more offset to be used as boundary for fill - // we offset by half the perimeter spacing (to get to the actual infill boundary) - // and then we offset back and forth by half the infill spacing to only consider the - // non-collapsing regions - coord_t inset = - (loop_number < 0) ? 0 : - (loop_number == 0) ? - // one loop - ext_perimeter_spacing : - // two or more loops? - perimeter_spacing; - coord_t top_inset = inset; + // create one more offset to be used as boundary for fill + // we offset by half the perimeter spacing (to get to the actual infill boundary) + // and then we offset back and forth by half the infill spacing to only consider the + // non-collapsing regions + coord_t inset = + (loop_number < 0) ? 0 : + (loop_number == 0) ? + // one loop + ext_perimeter_spacing : + // two or more loops? + perimeter_spacing; + coord_t top_inset = inset; - top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); - if(is_topmost_layer || is_bottom_layer) - inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); - else - inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset)))); + top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); + if(is_topmost_layer || is_bottom_layer) + inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); + else + inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset)))); - // simplify infill contours according to resolution - Polygons pp; - for (ExPolygon& ex : infill_contour) - ex.simplify_p(m_scaled_resolution, &pp); - ExPolygons not_filled_exp = union_ex(pp); - // collapse too narrow infill areas - const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); + // simplify infill contours according to resolution + Polygons pp; + for (ExPolygon& ex : infill_contour) + ex.simplify_p(m_scaled_resolution, &pp); + ExPolygons not_filled_exp = union_ex(pp); + // collapse too narrow infill areas + const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); - ExPolygons infill_exp = offset2_ex( - not_filled_exp, - float(-min_perimeter_infill_spacing / 2.), - float(inset + min_perimeter_infill_spacing / 2.)); - // append infill areas to fill_surfaces - if (!top_expolygons.empty()) { - infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset))); - } - this->fill_surfaces->append(infill_exp, stInternal); - - apply_extra_perimeters(infill_exp); - - // BBS: get the no-overlap infill expolygons - { - ExPolygons polyWithoutOverlap; - polyWithoutOverlap = offset2_ex( + ExPolygons infill_exp = offset2_ex( not_filled_exp, float(-min_perimeter_infill_spacing / 2.), - float(+min_perimeter_infill_spacing / 2.)); - if (!top_expolygons.empty()) - polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons); - this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end()); - } + float(inset + min_perimeter_infill_spacing / 2.)); + // append infill areas to fill_surfaces + if (!top_expolygons.empty()) { + infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset))); + } + result.infill = std::move(infill_exp); + + // BBS: get the no-overlap infill expolygons + { + ExPolygons polyWithoutOverlap; + polyWithoutOverlap = offset2_ex( + not_filled_exp, + float(-min_perimeter_infill_spacing / 2.), + float(+min_perimeter_infill_spacing / 2.)); + if (!top_expolygons.empty()) + polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons); + result.no_overlap = std::move(polyWithoutOverlap); + } + } + }); + for (ArachneSurfaceResult &result : results) { + if (result.has_loops) + // Moved, not copied: append(const ExtrusionEntity &) clones the whole wall tree of the island. + this->loops->append(std::move(result.loops)); + this->fill_surfaces->append(result.infill, stInternal); + apply_extra_perimeters(result.infill); + append(*this->fill_no_overlap, std::move(result.no_overlap)); } } diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index 8e7ca2ed91..9c5801985e 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -34,7 +34,7 @@ public: explicit Polygon(const Points &points) : MultiPoint(points) {} Polygon(std::initializer_list points) : MultiPoint(points) {} Polygon(const Polygon &other) : MultiPoint(other.points) {} - Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {} + Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {} static Polygon new_scale(const std::vector &points) { Polygon pgn; pgn.points.reserve(points.size()); @@ -43,7 +43,7 @@ public: return pgn; } Polygon& operator=(const Polygon &other) { points = other.points; return *this; } - Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; } + Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; } Point& operator[](Points::size_type idx) { return this->points[idx]; } const Point& operator[](Points::size_type idx) const { return this->points[idx]; } diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 29c795db33..3b59ab6c4c 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -26,7 +26,7 @@ class Polyline : public MultiPoint { public: Polyline() {}; Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {} - Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {} + Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {} Polyline(std::initializer_list list) : MultiPoint(list) { fitting_result.clear(); } @@ -47,7 +47,7 @@ public: fitting_result = other.fitting_result; return *this; } - Polyline& operator=(Polyline&& other) { + Polyline& operator=(Polyline&& other) noexcept { points = std::move(other.points); fitting_result = std::move(other.fitting_result); return *this; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 150e987bcb..e48f8d71f1 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,7 @@ #include #include +#include #include #include @@ -1930,7 +1932,9 @@ void PrintObject::detect_surfaces_type() bool interface_shells = ! spiral_mode && m_config.interface_shells.value; size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size(); - for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + // The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the + // others, so the regions run next to each other instead of one after another, each still over all layers. + tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) { BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start"; #ifdef SLIC3R_DEBUG_SLICE_PROCESSING for (Layer *layer : m_layers) @@ -1988,7 +1992,7 @@ void PrintObject::detect_surfaces_type() if (upper_layer) { ExPolygons upper_slices = interface_shells ? diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) : - diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes); + diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes); surfaces_append(top, opening_ex(upper_slices, offset), stTop); } else { // if no upper layer, all surfaces of this one are solid @@ -2014,7 +2018,7 @@ void PrintObject::detect_surfaces_type() surfaces_append( bottom, opening_ex( - diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes), + diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes), offset), surface_type_bottom_other); // if user requested internal shells, we need to identify surfaces @@ -2045,34 +2049,44 @@ void PrintObject::detect_surfaces_type() // and top surfaces; let's do an intersection to discover them and consider them // as bottom surfaces (to allow for bridge detection) if (! top.empty() && ! bottom.empty()) { - const auto cracks = intersection_ex(top, bottom); + const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom)); if (!cracks.empty()) { if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5; + // Only the bottom surfaces near a crack can take part: one that contains it must contain its box, + // and one whose box misses the grown crack is left unchanged by removing it. A layer cut through + // a fine relief has thousands of both, which made this loop quadratic. for (const auto& crack : cracks) { if (offset_ex(crack, small_crack_threshold).empty()) { // For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well - if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) { + const BoundingBox crack_bbox = get_extents(crack); + if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) { const auto& se = s.expolygon; - return diff_ex(crack, se, ApplySafetyOffset::Yes).empty() + return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox) + && diff_ex(crack, se, ApplySafetyOffset::Yes).empty() && se.area() > crack.area() * 2 && !offset_ex(diff_ex(se, crack), small_crack_threshold).empty(); })) continue; // Crack too small, leave it as part of the top surface, remove it from bottom surfaces + const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold); + const BoundingBox grown_bbox = get_extents(grown_crack); Surfaces bot_tmp; for (auto& b : bottom) { - surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type); + if (get_extents(b.expolygon).overlap(grown_bbox)) + surfaces_append(bot_tmp, diff_ex(b.expolygon, grown_crack), b.surface_type); + else + bot_tmp.emplace_back(std::move(b)); } bottom = std::move(bot_tmp); } } } - Polygons top_polygons = to_polygons(std::move(top)); + ExPolygons top_expolygons = to_expolygons(std::move(top)); top.clear(); - surfaces_append(top, diff_ex(top_polygons, bottom), stTop); + surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop); } } @@ -2163,7 +2177,7 @@ void PrintObject::detect_surfaces_type() { Polygons topbottom = to_polygons(top); polygons_append(topbottom, to_polygons(bottom)); - surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal); + surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal); } surfaces_append(surfaces_out, std::move(top)); @@ -2340,29 +2354,31 @@ void PrintObject::detect_surfaces_type() } } ); - // ============================================================================================================== - // === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ============== - // === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ========== - // === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ========== - // ============================================================================================================== - for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) { - tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), [this, region_id](const tbb::blocked_range &range) { - for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) { - Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces; - for (Surface &s : surfs) { - if (s.surface_type == stInternalAfterExternalBridge) { - s.surface_type = stBottomBridge; - } - } - } - } - ); - } } // ============================================================================================================== // === ORCA: End of second external bridge layer changes ======================================================= // ============================================================================================================== + }); // for each this->print->region_count + + // ============================================================================================================== + // === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ============== + // === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ========== + // === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ========== + // ============================================================================================================== + // Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below. + if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) { + tbb::parallel_for(tbb::blocked_range(0, m_layers.size()), [this](const tbb::blocked_range &range) { + for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) + for (LayerRegion *layerm : m_layers[idx_layer]->regions()) + for (Surface &s : layerm->slices.surfaces) + if (s.surface_type == stInternalAfterExternalBridge) + s.surface_type = stBottomBridge; + }); + m_print->throw_if_canceled(); + } + + tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) { BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start"; // Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces. tbb::parallel_for( @@ -2379,7 +2395,7 @@ void PrintObject::detect_surfaces_type() }); m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end"; - } // for each this->print->region_count + }); // Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.) m_typed_slices = true; @@ -2446,8 +2462,10 @@ void PrintObject::process_external_surfaces() BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end"; } - for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) { - BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start"; + BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start"; + // The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the + // others, so the regions run next to each other instead of one after another, each still over all layers. + tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) { tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), [this, &surfaces_covered, region_id](const tbb::blocked_range& range) { @@ -2462,9 +2480,9 @@ void PrintObject::process_external_surfaces() } } ); - m_print->throw_if_canceled(); - BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end"; - } + }); + m_print->throw_if_canceled(); + BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end"; } void PrintObject::discover_vertical_shells() @@ -2503,10 +2521,10 @@ void PrintObject::discover_vertical_shells() // The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit. return; BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom"; - //FIXME Improve the heuristics for a grain size. - size_t grain_size = std::max(num_layers / 16, size_t(1)); + // One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a + // few such layers next to each other must not end up in one task. tbb::parallel_for( - tbb::blocked_range(0, num_layers, grain_size), + tbb::blocked_range(0, num_layers, 1), [this, &cache_top_botom_regions](const tbb::blocked_range& range) { const std::initializer_list surfaces_bottom { stBottom, stBottomBridge }; const size_t num_regions = this->num_printing_regions(); @@ -2514,67 +2532,198 @@ void PrintObject::discover_vertical_shells() m_print->throw_if_canceled(); const Layer &layer = *m_layers[idx_layer]; DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer]; - // Simulate single set of perimeters over all merged regions. - float perimeter_offset = 0.f; - float perimeter_min_spacing = FLT_MAX; + const auto top_bottom_expansion = [&layer](size_t region_id) { + return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff; + }; #ifdef SLIC3R_DEBUG_SLICE_PROCESSING static size_t debug_idx = 0; ++ debug_idx; #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - for (size_t region_id = 0; region_id < num_regions; ++ region_id) { - LayerRegion &layerm = *layer.m_regions[region_id]; - float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff; - // Top surfaces. - append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion)); -// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion)); - // Bottom surfaces. - append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion)); -// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion)); - // Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only. - // First find the maxium number of perimeters per region slice. - unsigned int perimeters = 0; - for (Surface &s : layerm.slices.surfaces) - perimeters = std::max(perimeters, s.extra_perimeters); - perimeters += layerm.region().config().wall_loops.value; - // Then calculate the infill offset. - if (perimeters > 0) { - Flow extflow = layerm.flow(frExternalPerimeter); - Flow flow = layerm.flow(frPerimeter); - perimeter_offset = std::max(perimeter_offset, - 0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing()); - perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing()))); - } - polygons_append(cache.holes, to_polygons(layerm.fill_expolygons)); - } - // Save some computing time by reducing the number of polygons. - cache.top_surfaces = union_(cache.top_surfaces); - cache.bottom_surfaces = union_(cache.bottom_surfaces); - // For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print. - if (perimeter_offset > 0.) { - // The layer.lslices are forced to merge by expanding them first. - polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing)); + // The top surfaces, the bottom surfaces and the holes are independent of each other. + tbb::parallel_invoke( + [&]() { + for (size_t region_id = 0; region_id < num_regions; ++ region_id) + append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id))); +// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion)); + // Save some computing time by reducing the number of polygons. + cache.top_surfaces = union_(cache.top_surfaces); + }, + [&]() { + for (size_t region_id = 0; region_id < num_regions; ++ region_id) + append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id))); +// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion)); + cache.bottom_surfaces = union_(cache.bottom_surfaces); + }, + [&]() { + // Simulate single set of perimeters over all merged regions. + float perimeter_offset = 0.f; + float perimeter_min_spacing = FLT_MAX; + for (size_t region_id = 0; region_id < num_regions; ++ region_id) { + const LayerRegion &layerm = *layer.m_regions[region_id]; + // Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only. + // First find the maxium number of perimeters per region slice. + unsigned int perimeters = 0; + for (const Surface &s : layerm.slices.surfaces) + perimeters = std::max(perimeters, s.extra_perimeters); + perimeters += layerm.region().config().wall_loops.value; + // Then calculate the infill offset. + if (perimeters > 0) { + Flow extflow = layerm.flow(frExternalPerimeter); + Flow flow = layerm.flow(frPerimeter); + perimeter_offset = std::max(perimeter_offset, + 0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing()); + perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing()))); + } + polygons_append(cache.holes, to_polygons(layerm.fill_expolygons)); + } + // For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print. + if (perimeter_offset > 0.) { + // The layer.lslices are forced to merge by expanding them first. + polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing)); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING - { - Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices)); - svg.draw(layer.lslices, "blue"); - svg.draw(union_ex(cache.holes), "red"); - svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05)); - svg.Close(); - } + { + Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices)); + svg.draw(layer.lslices, "blue"); + svg.draw(union_ex(cache.holes), "red"); + svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05)); + svg.Close(); + } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - } - cache.holes = union_(cache.holes); + } + cache.holes = union_(cache.holes); + }); } }); m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom"; } - for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + // With one top/bottom cache for all regions, the shell and hole accumulation in the loop below depends on nothing + // region-specific but the shell settings and the external perimeter spacing, so a region sharing them with an earlier + // one reuses its result instead of repeating it: that accumulation is a union over several layers of top/bottom + // surfaces, and a multi-material print has a region per filament. + using AccumulationKey = std::array; + struct ShellAccumulation + { + AccumulationKey key; + Polygons shell; + Polygons holes; + }; + const auto accumulation_key = [](const PrintRegionConfig ®ion_config, const LayerRegion *layerm) { + return AccumulationKey{ double(region_config.top_shell_layers.value), region_config.top_shell_thickness.value, + double(region_config.bottom_shell_layers.value), region_config.bottom_shell_thickness.value, + double(layerm->flow(frExternalPerimeter).scaled_spacing()) }; + }; + const auto accumulate_shell = [this, &cache_top_botom_regions](size_t idx_layer, const PrintRegionConfig ®ion_config, + const LayerRegion *layerm, Polygons &shell, Polygons &holes) { + const Layer *layer = m_layers[idx_layer]; + polygons_append(holes, cache_top_botom_regions[idx_layer].holes); + auto combine_holes = [&holes](const Polygons &holes2) { + if (holes.empty() || holes2.empty()) + holes.clear(); + else + holes = intersection(holes, holes2); + }; + auto combine_shells = [&shell](const Polygons &shells2) { + if (shell.empty()) + shell = std::move(shells2); + else if (! shells2.empty()) { + polygons_append(shell, shells2); + // Running the union_ using the Clipper library piece by piece is cheaper + // than running the union_ all at once. + shell = union_(shell); + } + }; + static constexpr const bool one_more_layer_below_top_bottom_surfaces = false; + if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) { + // Gather top regions projected to this layer. + coordf_t print_z = layer->print_z; + int i = int(idx_layer) + 1; + int itop = int(idx_layer) + n_top_layers; + bool at_least_one_top_projected = false; + for (; i < int(cache_top_botom_regions.size()) && + (i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON); + ++ i) { + at_least_one_top_projected = true; + const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i]; + combine_holes(cache.holes); + combine_shells(cache.top_surfaces); + } + if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) { + // Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the + // boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one + // perimeter width of area + Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces, + layerm->flow(frExternalPerimeter).scaled_spacing()), + to_polygons(m_layers[i]->lslices)); + combine_shells(anchor_area); + } + + if (one_more_layer_below_top_bottom_surfaces) + if (i < int(cache_top_botom_regions.size()) && + (i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON)) + combine_holes(cache_top_botom_regions[i].holes); + } + if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) { + // Gather bottom regions projected to this layer. + coordf_t bottom_z = layer->bottom_z(); + int i = int(idx_layer) - 1; + int ibottom = int(idx_layer) - n_bottom_layers; + bool at_least_one_bottom_projected = false; + for (; i >= 0 && + (i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON); + -- i) { + at_least_one_bottom_projected = true; + const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i]; + combine_holes(cache.holes); + combine_shells(cache.bottom_surfaces); + } + + if (!at_least_one_bottom_projected && i >= 0) { + Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces, + layerm->flow(frExternalPerimeter).scaled_spacing()), + to_polygons(m_layers[i]->lslices)); + combine_shells(anchor_area); + } + + if (one_more_layer_below_top_bottom_surfaces) + if (i >= 0 && + (i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON)) + combine_holes(cache_top_botom_regions[i].holes); + } + }; + std::vector> shell_accumulations(top_bottom_surfaces_all_regions ? num_layers : 0); + if (! shell_accumulations.empty()) { + // Every (layer, key) pair is accumulated once, before the regions, so that nothing in the loop below is shared + // between them and they can run next to each other. + std::vector> todo; // layer, its slot, a region holding the key + for (size_t idx_layer = 0; idx_layer < num_layers; ++ idx_layer) { + std::vector &accumulations = shell_accumulations[idx_layer]; + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + if (this->printing_region(region_id).config().ensure_vertical_shell_thickness.value != evstAll) + continue; + const LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id]; + const AccumulationKey key = accumulation_key(layerm->region().config(), layerm); + if (std::none_of(accumulations.begin(), accumulations.end(), [&key](const ShellAccumulation &a) { return a.key == key; })) { + todo.push_back({ idx_layer, accumulations.size(), region_id }); + accumulations.push_back({ key, {}, {} }); + } + } + } + tbb::parallel_for(size_t(0), todo.size(), [this, &todo, &shell_accumulations, &accumulate_shell](size_t i) { + m_print->throw_if_canceled(); + const LayerRegion *layerm = m_layers[todo[i][0]]->m_regions[todo[i][2]]; + ShellAccumulation &out = shell_accumulations[todo[i][0]][todo[i][1]]; + accumulate_shell(todo[i][0], layerm->region().config(), layerm, out.shell, out.holes); + }); + m_print->throw_if_canceled(); + } + + const auto process_region = [&](size_t region_id) { const PrintRegion ®ion = this->printing_region(region_id); if (region.config().ensure_vertical_shell_thickness.value != evstAll ) // This region will be handled by discover_horizontal_shells(). - continue; + return; //FIXME Improve the heuristics for a grain size. size_t grain_size = std::max(num_layers / 16, size_t(1)); @@ -2614,7 +2763,7 @@ void PrintObject::discover_vertical_shells() grain_size = 1; tbb::parallel_for( tbb::blocked_range(0, num_layers, grain_size), - [this, region_id, &cache_top_botom_regions] + [this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell] (const tbb::blocked_range& range) { // printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end()); for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { @@ -2664,80 +2813,19 @@ void PrintObject::discover_vertical_shells() } } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - polygons_append(holes, cache_top_botom_regions[idx_layer].holes); - auto combine_holes = [&holes](const Polygons &holes2) { - if (holes.empty() || holes2.empty()) - holes.clear(); - else - holes = intersection(holes, holes2); - }; - auto combine_shells = [&shell](const Polygons &shells2) { - if (shell.empty()) - shell = std::move(shells2); - else if (! shells2.empty()) { - polygons_append(shell, shells2); - // Running the union_ using the Clipper library piece by piece is cheaper - // than running the union_ all at once. - shell = union_(shell); - } - }; - static constexpr const bool one_more_layer_below_top_bottom_surfaces = false; - if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) { - // Gather top regions projected to this layer. - coordf_t print_z = layer->print_z; - int i = int(idx_layer) + 1; - int itop = int(idx_layer) + n_top_layers; - bool at_least_one_top_projected = false; - for (; i < int(cache_top_botom_regions.size()) && - (i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON); - ++ i) { - at_least_one_top_projected = true; - const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i]; - combine_holes(cache.holes); - combine_shells(cache.top_surfaces); - } - if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) { - // Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the - // boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one - // perimeter width of area - Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces, - layerm->flow(frExternalPerimeter).scaled_spacing()), - to_polygons(m_layers[i]->lslices)); - combine_shells(anchor_area); - } - - if (one_more_layer_below_top_bottom_surfaces) - if (i < int(cache_top_botom_regions.size()) && - (i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON)) - combine_holes(cache_top_botom_regions[i].holes); - } - if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) { - // Gather bottom regions projected to this layer. - coordf_t bottom_z = layer->bottom_z(); - int i = int(idx_layer) - 1; - int ibottom = int(idx_layer) - n_bottom_layers; - bool at_least_one_bottom_projected = false; - for (; i >= 0 && - (i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON); - -- i) { - at_least_one_bottom_projected = true; - const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i]; - combine_holes(cache.holes); - combine_shells(cache.bottom_surfaces); - } - - if (!at_least_one_bottom_projected && i >= 0) { - Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces, - layerm->flow(frExternalPerimeter).scaled_spacing()), - to_polygons(m_layers[i]->lslices)); - combine_shells(anchor_area); - } - - if (one_more_layer_below_top_bottom_surfaces) - if (i >= 0 && - (i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON)) - combine_holes(cache_top_botom_regions[i].holes); - } + const AccumulationKey key = accumulation_key(region_config, layerm); + const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr : + [&]() -> const ShellAccumulation * { + for (const ShellAccumulation &a : shell_accumulations[idx_layer]) + if (a.key == key) + return &a; + return nullptr; + }(); + if (reused != nullptr) { + shell = reused->shell; + holes = reused->holes; + } else + accumulate_shell(idx_layer, region_config, layerm, shell, holes); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell)); @@ -2831,11 +2919,8 @@ void PrintObject::discover_vertical_shells() Polygons object_volume; Polygons internal_volume; { - Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{}; - Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ? - to_polygons(m_layers[idx_layer + 1]->lslices) : - Polygons{}; - object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice); + if (idx_layer > 0 && idx_layer + 1 < m_layers.size()) + object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices))); internal_volume = closing(polygonsInternal, SCALED_EPSILON); } @@ -2846,15 +2931,34 @@ void PrintObject::discover_vertical_shells() // the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy // 2. the area does not fully cover an internal polygon // This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small + // Both tests below compare a small piece against the whole layer. Done literally, that is + // quadratic in the number of pieces, which is what a layer split up by colour painting has, + // so each is restricted to the part of the layer near the piece with an identical result: + // object_volume is clipped to the piece's box, and only the internal polygons whose box meets + // the expanded piece take part in the count, since the others pass through the difference + // unchanged and add the same number to both sides of it. + std::vector internal_bboxes; + internal_bboxes.reserve(internal_volume.size()); + for (const Polygon &poly : internal_volume) + internal_bboxes.emplace_back(get_extents(poly)); regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(), - [&internal_volume, &min_perimeter_infill_spacing, + [&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing, &object_volume](const ExPolygon &p) { - return (p.area() < min_perimeter_infill_spacing * scaled(1.5) || - (p.area() < min_perimeter_infill_spacing * scaled(8.0) && - diff(to_polygons(p), object_volume).empty())) && - diff(internal_volume, - expand(to_polygons(p), min_perimeter_infill_spacing)) - .size() >= internal_volume.size(); + const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) || + (p.area() < min_perimeter_infill_spacing * scaled(8.0) && + diff(to_polygons(p), + ClipperUtils::clip_clipper_polygons_with_subject_bbox( + object_volume, get_extents(p).inflated(SCALED_EPSILON))) + .empty()); + if (!small) + return false; + const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing); + const BoundingBox bbox = get_extents(expanded); + Polygons nearby; + for (size_t i = 0; i < internal_volume.size(); ++i) + if (internal_bboxes[i].overlap(bbox)) + nearby.emplace_back(internal_volume[i]); + return diff(nearby, expanded).size() >= nearby.size(); }), regularized_shell.end()); } @@ -2876,8 +2980,9 @@ void PrintObject::discover_vertical_shells() #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ // Trim the internal & internalvoid by the shell. - Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell); - Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell); + const Polygons regularized_shell_polygons = to_polygons(regularized_shell); + Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons); + Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { @@ -2904,7 +3009,15 @@ void PrintObject::discover_vertical_shells() layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final"); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - } // for each region + }; // for each region + if (top_bottom_surfaces_all_regions) + // Nothing is shared between the regions, and a layer cut through a fine relief takes far longer than the others, + // so they run next to each other instead of one after another. + tbb::parallel_for(size_t(0), this->num_printing_regions(), process_region); + else + // Here every region fills the one top/bottom cache with its own surfaces first. + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) + process_region(region_id); } // void PrintObject::discover_vertical_shells() // #define DEBUG_BRIDGE_OVER_INFILL @@ -3425,6 +3538,16 @@ void PrintObject::bridge_over_infill() vertical_lines[i].b = Point{x, y_max}; } + // The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be + // hit. Leaving them out gives the same intersections without building a tree over the whole layer's + // boundary for every bridge. + const coord_t scan_x_min = bb_x.min.x(); + const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing; + anchors.erase(std::remove_if(anchors.begin(), anchors.end(), + [scan_x_min, scan_x_max](const Line &l) { + return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max; + }), + anchors.end()); auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer{std::move(anchors)}; auto bridged_area_tree = AABBTreeLines::LinesDistancer{to_lines(bridged_area)}; @@ -3669,26 +3792,58 @@ void PrintObject::bridge_over_infill() std::vector expanded_surfaces; expanded_surfaces.reserve(surfaces_by_layer[lidx].size()); + // The expanded fill boundary depends only on the bridging flow, and total_fill_area is not + // modified below, so build it once per spacing rather than once per candidate. A layer split + // into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each. + std::map boundary_by_spacing; + // expansion_area is a clean, non-overlapping set, so cutting a bridge out of it only changes the + // polygons near that bridge. The rest are passed through untouched 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) { + Polygons nearby; + for (const Polygon &p : polys) + (get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p); + return nearby; + }; for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) { const auto ®ion_config = candidate.region->region().config(); const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve || region_config.sparse_infill_pattern == ipOctagramSpiral; const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true); - ExPolygons bridge_components = intersection_ex(expand(candidate.new_polys, flow.scaled_spacing()), deep_infill_area); + const Polygons expanded_polys = expand(candidate.new_polys, flow.scaled_spacing()); + // deep_infill_area and internal_unsupported_area cover the whole layer; only their part under + // this candidate can change the results, so they are clipped to its box first. + ExPolygons bridge_components; + if (!expanded_polys.empty()) + bridge_components = intersection_ex(expanded_polys, + ClipperUtils::clip_clipper_polygons_with_subject_bbox( + deep_infill_area, get_extents(expanded_polys).inflated(SCALED_EPSILON))); // Orca: Filter whole bridge areas so their holes remain holes. bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(), [&internal_unsupported_area](const ExPolygon &component) { - return intersection_ex(component, internal_unsupported_area).empty(); + return intersection_ex(component, ClipperUtils::clip_clipper_polygons_with_subject_bbox( + internal_unsupported_area, + get_extents(component).inflated(SCALED_EPSILON))) + .empty(); }), bridge_components.end()); Polygons area_to_be_bridge = to_polygons(std::move(bridge_components)); - Polygons limiting_area = union_(area_to_be_bridge, expansion_area); - if (area_to_be_bridge.empty()) continue; - Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing())); + // Not split like the cut of expansion_area below: the whole limiting area is grown by 30% of the spacing, + // which merges neighbouring polygons, and any of its boundary can anchor the bridge. + Polygons limiting_area = union_(area_to_be_bridge, expansion_area); + + auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing()); + if (boundary_it == boundary_by_spacing.end()) + boundary_it = boundary_by_spacing + .emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing()))) + .first; + Polylines boundary_plines = boundary_it->second; { Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3f * flow.scaled_spacing())); boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end()); @@ -3764,9 +3919,12 @@ void PrintObject::bridge_over_infill() // Check collision with other expanded surfaces { bool reconstruct = false; - Polygons tmp_expanded_area = expand(bridging_area, 3.0f * flow.scaled_spacing()); + Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing()); + const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area); for (const CandidateSurface &s : expanded_surfaces) { - if (!intersection(s.new_polys, tmp_expanded_area).empty()) { + // Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer. + if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) && + !intersection(s.new_polys, tmp_expanded_area).empty()) { bridging_angle = s.bridge_angle; reconstruct = true; break; @@ -3790,10 +3948,20 @@ void PrintObject::bridge_over_infill() bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow, bridging_angle, scan_spacing, true)); } - bridging_area = intersection(bridging_area, limiting_area); - bridging_area = intersection(bridging_area, total_fill_area); - bridging_area = diff(bridging_area, total_top_area); - expansion_area = diff(expansion_area, bridging_area); + // Each of these meets one bridge with the whole layer, so the layer side is first cut down to the + // bridge's box (and expansion_area split as above); the result is the same. + if (!bridging_area.empty()) { + const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON); + bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox)); + bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox)); + bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox)); + } + if (!bridging_area.empty()) { + Polygons kept; + const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept); + append(kept, diff(cut, bridging_area)); + expansion_area = std::move(kept); + } #ifdef DEBUG_BRIDGE_OVER_INFILL debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r), diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index 813acecec4..8ac9172bfd 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1004,9 +1004,9 @@ public: ::fread(&y, sizeof(coord_t), 1, file); poly.points.emplace_back(Point(x * scale, y * scale)); } + printf("Polygon %d, area: %lf\n", i, area(poly.points)); if (which == -1 || which == i) m_support_polygons_deserialized.emplace_back(std::move(poly)); - printf("Polygon %d, area: %lf\n", i, area(poly.points)); } ::fread(&n_polygons, 4, 1, file); m_trimming_polygons_deserialized.reserve(n_polygons); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 70ab0b06f1..1d3f0b29ea 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -996,16 +996,46 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) if (is_auto(stype) && config_detect_sharp_tails) { // BBS detect sharp tail + // On a belt, "below" has to include the belt itself and the + // shear-advanced lower layer, or every belt-contact island reads as + // a sharp tail -- which is what the empty-predecessor skip above was + // really masking. effective_lower is exactly that notion of below. + const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; + // Each island is tested only against the lower islands whose box meets its own; overlaps() tries + // every pair, which is quadratic in the island counts of the two layers. + std::vector lower_bboxes; + lower_bboxes.reserve(tail_lower.size()); + for (const ExPolygon &lower : tail_lower) + lower_bboxes.emplace_back(get_extents(lower)); for (const ExPolygon& expoly : curr_polys) { bool is_sharp_tail = false; // 1. nothing below - // this is a sharp tail region if it's floating and non-ignorable. - // On a belt, "below" has to include the belt itself and the - // shear-advanced lower layer, or every belt-contact island reads as - // a sharp tail -- which is what the empty-predecessor skip above was - // really masking. effective_lower is exactly that notion of below. - const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; - if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) { + // this is a sharp tail region if it's floating and non-ignorable + const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled); + const BoundingBox bbox = get_extents(expanded); + ExPolygons lower_nearby; + for (size_t i = 0; i < tail_lower.size(); ++i) + if (lower_bboxes[i].overlap(bbox)) + lower_nearby.emplace_back(tail_lower[i]); + // As overlaps(expanded, lower_nearby), with each lower island cut to the island's box first: + // below a fine relief the lower layer is a few islands with thousands of holes, and the whole + // of that boundary would otherwise be intersected once per island above. + const auto overlaps_nearby = [&]() { + for (const ExPolygon &a : expanded) { + if (a.empty()) + continue; + const BoundingBox a_bbox = get_extents(a); + for (const ExPolygon &b : lower_nearby) { + if (b.empty() || !get_extents(b).overlap(a_bbox)) + continue; + const Polygons b_near = ClipperUtils::clip_clipper_polygons_with_subject_bbox(b, a_bbox.inflated(SCALED_EPSILON)); + if (!intersection_pl(to_polylines(b_near), a).empty() || b.contains(a.contour.points.front())) + return true; + } + } + return false; + }; + if (!overlaps_nearby()) { is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty(); } diff --git a/src/libslic3r/Surface.hpp b/src/libslic3r/Surface.hpp index 5e0c0461e4..4564ba0301 100644 --- a/src/libslic3r/Surface.hpp +++ b/src/libslic3r/Surface.hpp @@ -68,7 +68,7 @@ public: thickness(other.thickness), thickness_layers(other.thickness_layers), bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters) {}; - Surface(Surface &&rhs) + Surface(Surface &&rhs) noexcept : surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)), thickness(rhs.thickness), thickness_layers(rhs.thickness_layers), bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters) @@ -94,7 +94,7 @@ public: return *this; } - Surface& operator=(Surface &&rhs) + Surface& operator=(Surface &&rhs) noexcept { surface_type = rhs.surface_type; expolygon = std::move(rhs.expolygon); diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index b0a5250532..e3662919e7 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -167,10 +167,10 @@ inline void append(std::vector &dest, std::vector &&src) { if (dest.empty()) dest = std::move(src); - else { - dest.reserve(dest.size() + src.size()); - std::move(std::begin(src), std::end(src), std::back_inserter(dest)); - } + else + // insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which + // made appending piece by piece quadratic. + dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end())); src.clear(); src.shrink_to_fit(); } diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 6ab08f797f..44afdece7c 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(${_TEST_NAME}_tests test_fill_plane_path.cpp test_adaptive_infill_field.cpp test_geometry.cpp + test_kdtree.cpp test_imex_arrange.cpp test_imex_helpers.cpp test_imex_zones.cpp diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp index c9026a9ba2..4f8d872ae5 100644 --- a/tests/libslic3r/test_clipper_utils.cpp +++ b/tests/libslic3r/test_clipper_utils.cpp @@ -7,12 +7,18 @@ #include "libslic3r/libslic3r.h" #include #include +#include #include #include #include +#include +#include #include +#include #include +#include +#include #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Point.hpp" @@ -300,3 +306,331 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") { REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested))); REQUIRE(top_level_expolygons(reference).size() == 1); } + +// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two +// encodings of the same geometry compare equal however the pieces came back or wherever a ring started. +static std::vector> canonical_rings(const ExPolygons &expolygons) +{ + std::vector> rings; + const auto add = [&rings](const Polygon &poly) { + if (poly.points.empty()) + return; + Points pts = poly.points; + std::rotate(pts.begin(), + std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) { + return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y()); + }), + pts.end()); + std::vector flat; + flat.reserve(pts.size() * 2); + for (const Point &p : pts) { + flat.emplace_back(p.x()); + flat.emplace_back(p.y()); + } + rings.emplace_back(std::move(flat)); + }; + for (const ExPolygon &expoly : expolygons) { + add(expoly.contour); + for (const Polygon &hole : expoly.holes) + add(hole); + } + std::sort(rings.begin(), rings.end()); + 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; + const coord_t cell = scaled(2.), side = scaled(1.5), frame = scaled(0.3); + ExPolygons subject; + for (int y = 0; y < n; ++ y) + for (int x = 0; x < n; ++ x) { + const Point o(x * cell, y * cell); + ExPolygon square(Polygon({ o, o + Point(side, 0), o + Point(side, side), o + Point(0, side) })); + Polygon hole({ o + Point(frame, frame), o + Point(frame, side - frame), o + Point(side - frame, side - frame), o + Point(side - frame, frame) }); + square.holes.emplace_back(std::move(hole)); + subject.emplace_back(std::move(square)); + } + // Clip polygons crossing many squares, one of them large with holes of its own. + Polygons clip; + const coord_t span = n * cell; + for (int i = 0; i < 8; ++ i) { + const coord_t y0 = coord_t(i) * span / 8, y1 = y0 + scaled(0.9); + clip.emplace_back(Polygon({ Point(- cell, y0), Point(span, y0 + cell * 3), Point(span, y1 + cell * 3), Point(- cell, y1) })); + } + ExPolygon big(Polygon({ Point(span / 4, span / 4), Point(3 * span / 4, span / 4), Point(3 * span / 4, 3 * span / 4), Point(span / 4, 3 * span / 4) })); + for (int i = 0; i < 4; ++ i) { + const Point o(span / 4 + scaled(3.1) + i * scaled(9.7), span / 4 + scaled(5.3)); + big.holes.emplace_back(Polygon({ o, o + Point(0, scaled(20.)), o + Point(scaled(5.), scaled(20.)), o + Point(scaled(5.), 0) })); + } + polygons_append(clip, to_polygons(big)); + + const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes); + + // The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled + // 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(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(same_rings(intersection_tiled, intersection_plain, tolerance)); +} + +// An outline with a small clockwise hole well inside it, cut along its edge by three clip polygons that never reach the +// hole. Clipper2 puts the hole at the top level of its PolyTree, beside the outline rather than under it. The coordinates +// come from a sliced layer reduced to the few paths that still do this; most changes to them don't. +static const Polygons misnesting_subject{ + Polygon({ { 23000000, 31000000 }, { 7500000, 25900000 }, { 200000, 26450004 }, { 0, 26450004 }, { 0, 26550004 }, + { -100000, 26550004 }, { -300000, 26600000 }, { 13000000, 0 } }), + Polygon({ { 10200000, 24250000 }, { 10500000, 24250000 }, { 9800000, 24000000 } }), + Polygon({ { 4500000, 22500000 }, { 5000000, 22000000 }, { 4000000, 22000000 } }), +}; +static const Polygons misnesting_clip{ + Polygon({ { 0, 26550004 }, { -200000, 26550004 }, { -200000, 26000000 } }), + Polygon({ { 0, 26450004 }, { 100000, 26450004 }, { 0, 26000000 } }), + Polygon({ { 10000000, 24000000 }, { 10199999, 24250000 }, { 10500000, 24250000 }, { 8000000, 26500000 } }), +}; + +// The case above inside a frame, a square outline with a square hole around the case, and with an island in its hole. +// Clipper2 then puts the hole beside the outline inside the frame's hole, where the frame and the outline both enclose +// it, and the island under the misplaced hole. +static Polygons misnesting_framed_subject() +{ + Polygons subject = misnesting_subject; + subject.emplace_back(Polygon({ { -2000000, -2000000 }, { 25000000, -2000000 }, { 25000000, 33000000 }, { -2000000, 33000000 } })); + subject.emplace_back(Polygon({ { -1000000, -1000000 }, { -1000000, 32000000 }, { 24000000, 32000000 }, { 24000000, -1000000 } })); + subject.emplace_back(Polygon({ { 4400000, 22100000 }, { 4600000, 22100000 }, { 4500000, 22300000 } })); + return subject; +} +// In the hole of the framed case, beside its island. +static const Point in_framed_hole(4500000, 22400000); +// In the island of the framed case. +static const Point in_framed_island(4500000, 22150000); + +static void check_orientation(const ExPolygons &expolygons) +{ + for (const ExPolygon &expoly : expolygons) { + CHECK(expoly.contour.is_counter_clockwise()); + for (const Polygon &hole : expoly.holes) + CHECK(hole.is_clockwise()); + } +} + +// How many of the expolygons contain the point, each on its own, as a per-piece operation sees them. +static size_t pieces_containing(const ExPolygons &expolygons, const Point &pt) +{ + return std::count_if(expolygons.begin(), expolygons.end(), [&pt](const ExPolygon &expoly) { return expoly.contains(pt); }); +} + +TEST_CASE("Every hole of a boolean result belongs to the expolygon around it", "[ClipperUtils]") { + const Polygon &hole = misnesting_subject.back(); + REQUIRE(hole.is_clockwise()); + const ExPolygons result = diff_ex(misnesting_subject, misnesting_clip); + check_orientation(result); + CHECK(pieces_containing(result, hole.centroid()) == 0); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(misnesting_subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A misplaced hole goes to the innermost contour around it, an island in it stays filled", "[ClipperUtils]") { + const Polygons subject = misnesting_framed_subject(); + const ExPolygons result = diff_ex(subject, misnesting_clip); + check_orientation(result); + // Given to the frame instead of the outline, the hole would leave the outline covering it. + CHECK(pieces_containing(result, in_framed_hole) == 0); + CHECK(pieces_containing(result, in_framed_island) == 1); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A misplaced hole touching the contour around it goes to that contour", "[ClipperUtils]") { + // The first two vertices of the hole the subject's triangle cuts round a fraction of a unit outside the outline, the + // third is well inside it. Reduced from a sliced layer; Clipper2 puts the hole at the top level of its PolyTree. + const Polygons subject{ + Polygon({ { 1000000, 5000000 }, { 1000000, 4000000 }, { 6000000, 0 } }), + Polygon({ { 1024999, 4500000 }, { 1100011, 4574998 }, { 1100011, 4450000 } }), + }; + const Polygons clip{ + Polygon({ { 1500000, 5000000 }, { 1062508, 4612507 }, { 1212486, 4462504 } }), + Polygon({ { 1100000, 4430000 }, { 1024998, 4500001 }, { 2000000, 3000000 } }), + }; + const ExPolygons result = diff_ex(subject, clip); + check_orientation(result); + CHECK(pieces_containing(result, subject.back().centroid()) == 0); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, clip)), 1e-9)); +} + +TEST_CASE("A misplaced island comes out as an expolygon of its own", "[ClipperUtils]") { + // Clipper2 puts the island of this result directly under the outer contour, at the level of its holes, instead of + // under the hole around it. Reduced from a sliced layer. + const Polygons subject{ + Polygon({ { 5200000, 700000 }, { 0, 5000000 }, { 0, 0 } }), + Polygon({ { 3100000, 825000 }, { 3250000, 900001 }, { 3250000, 1000000 }, { 4000000, 1500000 }, { 3000000, 500000 } }), + }; + const Polygons clip{ + Polygon({ { 4500000, 1200000 }, { 4000000, 1000000 }, { 5000000, 800000 } }), + Polygon({ { 3250000, 900001 }, { 3250000, 1000000 }, { 3000000, 1000000 }, { 3500000, 1100000 }, { 3100000, 1000000 }, { 3183343, 866672 } }), + Polygon({ { 4000000, 1000000 }, { 4000000, 1000000 }, { 4000000, 1000000 } }), + }; + const ExPolygons result = diff_ex(subject, clip); + check_orientation(result); + // In the island. + CHECK(pieces_containing(result, Point(3230000, 1020000)) == 1); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, clip)), 1e-9)); +} + +TEST_CASE("Nested islands of an even-odd union keep their misplaced holes", "[ClipperUtils]") { + // The framed case as one even-odd union: the frame is the top level, everything in its hole is nested, and Clipper2 + // puts the case's hole beside the outline among the nested islands. + ExPolygons input; + for (const Polygon &polygon : misnesting_framed_subject()) + input.emplace_back(polygon); + for (const Polygon &polygon : misnesting_clip) + input.emplace_back(polygon); + ExPolygons nested; + const ExPolygons top_level = top_level_expolygons(input, &nested); + REQUIRE(top_level.size() == 1); + check_orientation(nested); + CHECK(pieces_containing(nested, in_framed_hole) == 0); + CHECK(pieces_containing(nested, in_framed_island) == 1); +} + +TEST_CASE("Loops chained outside in keep their nesting where Clipper2 misplaces one", "[ClipperUtils]") { + // An even-odd union of the case's paths leaves the hole at the top level of the PolyTree, among the outer loops. + Polygons loops = misnesting_subject; + polygons_append(loops, misnesting_clip); + const Polygons chained = union_pt_chained_outside_in(loops); + REQUIRE(chained.size() == 7); + for (const Polygon &loop : chained) + CHECK(loop.is_counter_clockwise()); + // The hole's loop comes right after the outline's, which has no other loop inside it. + const auto outline = std::max_element(chained.begin(), chained.end(), [](const Polygon &a, const Polygon &b) { return a.area() < b.area(); }); + REQUIRE(outline + 1 != chained.end()); + CHECK((outline + 1)->area() == Catch::Approx(- misnesting_subject.back().area())); +} + +TEST_CASE("A misplaced hole hugging an island inside it still goes to the outline", "[ClipperUtils]") { + // The case's hole with an island filling most of it, 20 units from it: the hole's vertices all lie close to the + // island and its middle inside it, yet the island is inside the hole, not around it. + Polygons subject = misnesting_subject; + subject.emplace_back(Polygon({ { 4000020, 22000010 }, { 4500000, 22150000 }, { 4999980, 22000010 }, + { 4650000, 22160000 }, { 4500000, 22499980 }, { 4350000, 22160000 } })); + const ExPolygons result = diff_ex(subject, misnesting_clip); + check_orientation(result); + // In the hole below the island, and in the island. + CHECK(pieces_containing(result, Point(4500000, 22050000)) == 0); + CHECK(pieces_containing(result, Point(4500000, 22300000)) == 1); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A concave misplaced hole touching the outline at every vertex goes to it", "[ClipperUtils]") { + // The case with the outline drawn in to two more vertices and the hole replaced by a concave pentagon, each of its + // vertices within 3 units of the outline; the middle of its bounding box is outside both. + Polygons subject = misnesting_subject; + subject.front().points.insert(subject.front().points.end(), { { 7000000, 20000000 }, { 20000000, 22000000 } }); + subject.back() = Polygon({ { 1500002, 22999999 }, { 20333332, 22999999 }, { 19999999, 22000001 }, { 6999998, 20000001 }, { 12999995, 13 } }); + const ExPolygons result = diff_ex(subject, misnesting_clip); + check_orientation(result); + CHECK(pieces_containing(result, Point(10000000, 22500000)) == 0); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A point strictly inside a polygon, where there is one", "[ClipperUtils]") { + // A notch with a horizontal edge at the middle of its bounding box, so a line through that middle runs along the edge; + // two lobes joined by a one unit neck and a square with a one unit spike, where the widest band between vertex heights + // runs through the thin part. + const Polygons wide{ + Polygon({ { 0, 0 }, { 2000000, 0 }, { 2000000, 5000000 }, { 8000000, 5000000 }, { 8000000, 0 }, { 10000000, 0 }, + { 10000000, 10000000 }, { 0, 10000000 } }), + Polygon({ { 0, 0 }, { 100, 0 }, { 100, 5 }, { 51, 5 }, { 51, 25 }, { 100, 25 }, { 100, 30 }, { 0, 30 }, { 0, 25 }, + { 50, 25 }, { 50, 5 }, { 0, 5 } }), + Polygon({ { 0, 0 }, { 1000000, 0 }, { 1000000, 1000000 }, { 500001, 1000000 }, { 500001, 5000000 }, { 500000, 5000000 }, + { 500000, 1000000 }, { 0, 1000000 } }), + }; + for (Polygon polygon : wide) + for (int winding = 0; winding < 2; ++ winding, polygon.reverse()) { + const std::optional pt = ClipperUtils::interior_point(polygon); + REQUIRE(pt); + CHECK(contains(polygon, *pt, false)); + } + // One unit thick everywhere, these hold no grid point strictly inside: a strip, and rows one unit high joined at the + // left. + CHECK_FALSE(ClipperUtils::interior_point(Polygon({ { 0, 0 }, { 1000, 0 }, { 1000, 1 }, { 0, 1 } }))); + Polygon rows({ { 0, 0 }, { 100, 0 } }); + for (coord_t y = 1; y < 100; y += 2) + rows.points.insert(rows.points.end(), { { 100, y }, { 1, y }, { 1, y + 1 }, { 100, y + 1 } }); + rows.points.emplace_back(0, 100); + CHECK_FALSE(ClipperUtils::interior_point(rows)); +} + +TEST_CASE("A correctly nested result converts in the order of its tree", "[ClipperUtils]") { + // Two squares, one with a hole holding an island, the other with two holes and a corner cut off. Clipper2 nests this + // correctly, so the expolygons follow its tree, each path as Clipper2 returned it: an outline with its holes in tree + // order, then the islands of those holes, outline by outline. + const Polygons subject{ + Polygon({ { 0, 0 }, { 10000000, 0 }, { 10000000, 10000000 }, { 0, 10000000 } }), + Polygon({ { 2000000, 2000000 }, { 2000000, 8000000 }, { 8000000, 8000000 }, { 8000000, 2000000 } }), + Polygon({ { 4000000, 4000000 }, { 6000000, 4000000 }, { 6000000, 6000000 }, { 4000000, 6000000 } }), + Polygon({ { 20000000, 0 }, { 32000000, 0 }, { 32000000, 12000000 }, { 20000000, 12000000 } }), + Polygon({ { 21000000, 1000000 }, { 21000000, 4000000 }, { 24000000, 4000000 }, { 24000000, 1000000 } }), + Polygon({ { 26000000, 6000000 }, { 26000000, 10000000 }, { 30000000, 10000000 }, { 30000000, 6000000 } }), + }; + const Polygons clip{ Polygon({ { 31000000, -1000000 }, { 33000000, -1000000 }, { 33000000, 1000000 }, { 31000000, 1000000 } }) }; + ExPolygons expected(3); + expected[0].contour = Polygon({ { 31000000, 1000000 }, { 32000000, 1000000 }, { 32000000, 12000000 }, { 20000000, 12000000 }, + { 20000000, 0 }, { 31000000, 0 } }); + expected[0].holes = { Polygon({ { 26000000, 10000000 }, { 30000000, 10000000 }, { 30000000, 6000000 }, { 26000000, 6000000 } }), + Polygon({ { 21000000, 4000000 }, { 24000000, 4000000 }, { 24000000, 1000000 }, { 21000000, 1000000 } }) }; + expected[1].contour = Polygon({ { 10000000, 10000000 }, { 0, 10000000 }, { 0, 0 }, { 10000000, 0 } }); + expected[1].holes = { Polygon({ { 2000000, 8000000 }, { 8000000, 8000000 }, { 8000000, 2000000 }, { 2000000, 2000000 } }) }; + expected[2].contour = Polygon({ { 6000000, 6000000 }, { 4000000, 6000000 }, { 4000000, 4000000 }, { 6000000, 4000000 } }); + CHECK(diff_ex(subject, clip) == expected); +} + +TEST_CASE("Loops of a correctly nested union chain in the order of its tree", "[ClipperUtils]") { + // Three nested squares and two more beside them, as concentric fill hands them over: chained outside in, depth first, + // each loop as Clipper2 returned it, the holes reversed to run counter-clockwise. + const Polygons loops{ + Polygon({ { 0, 0 }, { 10000000, 0 }, { 10000000, 10000000 }, { 0, 10000000 } }), + Polygon({ { 1000000, 1000000 }, { 9000000, 1000000 }, { 9000000, 9000000 }, { 1000000, 9000000 } }), + Polygon({ { 2000000, 2000000 }, { 8000000, 2000000 }, { 8000000, 8000000 }, { 2000000, 8000000 } }), + Polygon({ { 20000000, 0 }, { 32000000, 0 }, { 32000000, 12000000 }, { 20000000, 12000000 } }), + Polygon({ { 21000000, 1000000 }, { 31000000, 1000000 }, { 31000000, 11000000 }, { 21000000, 11000000 } }), + }; + const Polygons expected{ + Polygon({ { 10000000, 10000000 }, { 0, 10000000 }, { 0, 0 }, { 10000000, 0 } }), + Polygon({ { 9000000, 1000000 }, { 9000000, 9000000 }, { 1000000, 9000000 }, { 1000000, 1000000 } }), + Polygon({ { 8000000, 8000000 }, { 2000000, 8000000 }, { 2000000, 2000000 }, { 8000000, 2000000 } }), + Polygon({ { 32000000, 12000000 }, { 20000000, 12000000 }, { 20000000, 0 }, { 32000000, 0 } }), + Polygon({ { 31000000, 1000000 }, { 31000000, 11000000 }, { 21000000, 11000000 }, { 21000000, 1000000 } }), + }; + CHECK(union_pt_chained_outside_in(loops) == expected); +} + +TEST_CASE("A clockwise sliver with nothing around it comes back as Clipper2 returned it", "[ClipperUtils]") { + // Rounding leaves a clockwise sliver of half a square unit beside the result's one piece, outside it. Reduced from a + // sliced layer. It is no hole of anything, so it stays a contour of its own, neither dropped nor given to the piece. + const Polygons subject{ Polygon({ { 875000, 200000 }, { 900002, 175002 }, { 950001, 150004 } }) }; + const Polygons clip{ Polygon({ { 879409, 185299 }, { 950000, 150004 }, { 1000000, 200000 } }) }; + const ExPolygons result = diff_ex(subject, clip); + REQUIRE(result.size() == 2); + CHECK(std::count_if(result.begin(), result.end(), [](const ExPolygon &expoly) { return expoly.contour.is_clockwise() && expoly.holes.empty(); }) == 1); + CHECK(std::all_of(result.begin(), result.end(), [](const ExPolygon &expoly) { return expoly.holes.empty(); })); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, clip)), 1e-9)); +} diff --git a/tests/libslic3r/test_kdtree.cpp b/tests/libslic3r/test_kdtree.cpp new file mode 100644 index 0000000000..fee8c0e98a --- /dev/null +++ b/tests/libslic3r/test_kdtree.cpp @@ -0,0 +1,72 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "libslic3r/KDTreeIndirect.hpp" +#include "libslic3r/Point.hpp" + +using namespace Slic3r; + +TEST_CASE("Visiting the nearby points gives what collecting them gives", "[KDTree]") { + std::mt19937 rng(19937); + std::uniform_real_distribution coord(-50.f, 50.f); + // Points in a box, so that a radius search returns anything from none of them to all of them. + std::vector points(2000); + for (Vec3f &p : points) + p = Vec3f(coord(rng), coord(rng), coord(rng)); + + auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); }; + KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate); + std::vector indices(points.size()); + std::iota(indices.begin(), indices.end(), 0); + tree.build(indices); + + const float radius = GENERATE(0.5f, 5.f, 25.f, 200.f); + for (int i = 0; i < 20; ++ i) { + const Vec3f center(coord(rng), coord(rng), coord(rng)); + + const std::vector collected = find_nearby_points(tree, center, radius); + std::vector visited; + visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); }); + + // Same points, and in the same order: a caller that keeps the first of several equally good ones + // must get the same answer either way. + REQUIRE(visited == collected); + } +} + +TEST_CASE("A radius search returns every point within the radius and no other", "[KDTree]") { + std::mt19937 rng(2024); + std::uniform_real_distribution coord(-20.f, 20.f); + std::vector points(500); + for (Vec3f &p : points) + p = Vec3f(coord(rng), coord(rng), coord(rng)); + + auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); }; + KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate); + std::vector indices(points.size()); + std::iota(indices.begin(), indices.end(), 0); + tree.build(indices); + + const Vec3f center(1.f, -2.f, 3.f); + const float radius = 7.f; + + std::vector expected; + for (size_t i = 0; i < points.size(); ++ i) + if ((points[i] - center).squaredNorm() < radius * radius) + expected.emplace_back(i); + + std::vector visited; + visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); }); + std::sort(visited.begin(), visited.end()); + + REQUIRE(! expected.empty()); + REQUIRE(visited == expected); +}