diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp index 2ba9ae5490..d52a326e2e 100644 --- a/src/libslic3r/GCode/OrderingStrategies.cpp +++ b/src/libslic3r/GCode/OrderingStrategies.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -134,15 +135,79 @@ 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; with thousands of islands on a layer the all-pairs + // scan, repeated after every reversal, never finished. Rebuilding the grid costs more than it saves on small inputs. + 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); bool improved = false; + // Reversing between two segments that only touch or overlap along a line need not remove the intersection, and on + // islands laid out on a regular grid (a tiled texture, an array of parts) the loop cycled through the same orderings + // until the pn * pn cap - effectively forever. Stop as soon as an ordering repeats: until then this is the same loop. + std::unordered_set seen_paths; + const auto path_hash = [&path]() { + uint64_t h = 1469598103934665603ull; // FNV-1a + for (size_t idx : path) + h = (h ^ uint64_t(idx)) * 1099511628211ull; + return h; + }; + seen_paths.insert(path_hash()); while (max_iters-- > 0) { - auto [ci, cj] = find_crossing(); + auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing(); if (ci == std::numeric_limits::max()) break; improved = true; std::reverse(path.begin() + ci + 1, path.begin() + cj + 1); + if (!seen_paths.insert(path_hash()).second) + break; } return improved; } diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 8b1d612b00..f7c7597d76 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1311,10 +1311,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]); } @@ -1322,7 +1327,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); @@ -1847,6 +1852,54 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto } } +// diff_ex(subject, clip), one subject ExPolygon at a time against the part of `clip` in its box. The pieces are disjoint, +// so the result is the same; but ClipperLib never gets a whole finely painted layer at once, where re-linking the holes +// of its PolyTree (FixupFirstLefts) is quadratic in the thousands of regions. +static ExPolygons diff_ex_by_piece(const ExPolygons &subject, const ExPolygons &clip) +{ + // A coarse grid over the clip's boxes, so each piece only looks at the clip regions near it. + std::vector clip_bboxes; + clip_bboxes.reserve(clip.size()); + BoundingBox extent; + for (const ExPolygon &expoly : clip) { + clip_bboxes.emplace_back(get_extents(expoly)); + extent.merge(clip_bboxes.back()); + } + constexpr int GRID = 64; + const Point size = extent.defined ? extent.size() : Point(1, 1); + const coord_t cell_w = std::max(1, size.x() / GRID + 1), cell_h = std::max(1, size.y() / GRID + 1); + const auto cells = [&](const BoundingBox &bb, auto &&fn) { + const int x0 = std::clamp(int((bb.min.x() - extent.min.x()) / cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - extent.min.x()) / cell_w), 0, GRID - 1); + const int y0 = std::clamp(int((bb.min.y() - extent.min.y()) / cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - extent.min.y()) / cell_h), 0, GRID - 1); + for (int y = y0; y <= y1; ++y) + for (int x = x0; x <= x1; ++x) + fn(y * GRID + x); + }; + std::vector> grid(GRID * GRID); + if (extent.defined) + for (size_t i = 0; i < clip.size(); ++i) + cells(clip_bboxes[i], [&](int cell) { grid[cell].emplace_back(i); }); + + std::vector pieces(subject.size()); + tbb::parallel_for(size_t(0), subject.size(), [&](size_t idx) { + const BoundingBox bbox = get_extents(subject[idx]).inflated(SCALED_EPSILON); + std::vector near; + if (extent.defined && bbox.overlap(extent)) { + cells(bbox, [&](int cell) { append(near, grid[cell]); }); + sort_remove_duplicates(near); + } + Polygons nearby_clip; + for (size_t i : near) + if (clip_bboxes[i].overlap(bbox)) + polygons_append(nearby_clip, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip[i], bbox)); + pieces[idx] = nearby_clip.empty() ? ExPolygons{ subject[idx] } : diff_ex(subject[idx], nearby_clip); + }); + ExPolygons out; + for (ExPolygons &piece : pieces) + append(out, std::move(piece)); + return out; +} + static std::vector> merge_segmented_layers(const std::vector> &segmented_regions, std::vector> &&top_and_bottom_layers, const size_t num_facets_states, @@ -1861,18 +1914,18 @@ static std::vector> merge_segmented_layers(const std::ve 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) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { assert(segmented_regions[layer_idx].size() == num_facets_states); + ExPolygons top_and_bottom_all_colours; + for (const std::vector &top_and_bottom_by_extruder : top_and_bottom_layers) + append(top_and_bottom_all_colours, top_and_bottom_by_extruder[layer_idx]); // Zero is skipped because it is the default color of the volume 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]); - } - } - } + // All colours at once: taking them one after another made every piece go through ClipperLib once per + // colour, and it is the same area either way. + if (!top_and_bottom_all_colours.empty() && !segmented_regions_trimmed.empty()) + segmented_regions_trimmed = diff_ex_by_piece(segmented_regions_trimmed, top_and_bottom_all_colours); segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed); } @@ -2171,16 +2224,53 @@ 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. + 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]); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 519b6e826e..174c5e4fce 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -854,11 +854,41 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) if (is_auto(stype) && config_detect_sharp_tails) { // BBS detect sharp tail + // Each island is tested only against the lower islands whose box meets its own: overlaps() tries every + // pair, which on a layer cut through a fine relief (thousands of islands above thousands) never ends. + std::vector lower_bboxes; + lower_bboxes.reserve(lower_polys.size()); + for (const ExPolygon &lower : lower_polys) + 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 - if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) { + 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 < lower_polys.size(); ++i) + if (lower_bboxes[i].overlap(bbox)) + lower_nearby.emplace_back(lower_polys[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, whose whole boundary + // was otherwise intersected again for every 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(); }