diff --git a/docs/HLSD/multiline-infill.md b/docs/HLSD/multiline-infill.md index d644b197eb..2f68fe3ef9 100644 --- a/docs/HLSD/multiline-infill.md +++ b/docs/HLSD/multiline-infill.md @@ -40,6 +40,12 @@ The pattern is phased on fixed positions, so it lines up across layers and across the regions of one layer. Rounding the corners with `sparse_infill_smooth_factor` happens before `multiline_fill()`. +Each region builds only the rows over its bounding box in that frame, and +outlines only the centerlines within `d1 / 2` of it, the ones whose outlines +reach it. Every row is monotone along its direction, so each outline is started +on the cap at the first end of its centerline, outside the region, and clipping +to the region cuts it only where it crosses the boundary. + ## Cubic Single-line Cubic draws the three families at the same spacing `h` and shifts diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 55ae50f9bb..998cfd9a7e 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -3094,7 +3094,6 @@ static std::vector cubic_upper_level(double tau, double h, double period, bool FillRectilinear::fill_surface_trapezoidal( const Surface* surface, FillParams params, - const std::initializer_list& sweep_params, Polylines& polylines_out, int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic { @@ -3126,9 +3125,20 @@ bool FillRectilinear::fill_surface_trapezoidal( expolygon.rotate(-base_angle, rotate_vector.second); } - // Use extended object bounding box for consistent pattern across layers - BoundingBox bb = this->extended_object_bounding_box(); const size_t infill_layer_id = (surface->thickness_layers > 0) ? this->layer_id / surface->thickness_layers : this->layer_id; + // The triangular family turns by 120 degrees every layer, about the origin of the frame it is built in. + const size_t layer_mod = infill_layer_id % 3; + const double angle = layer_mod * 2.0 * M_PI / 3.0; + + // Only build the rows over the surface, seen in the frame they are built in. + Polygon local = expolygon.contour; + if (Pattern_type != 0) { + local.translate(-rotate_vector.second.x(), -rotate_vector.second.y()); + if (layer_mod) + local.rotate(-angle); + } + BoundingBox cover = get_extents(local); + cover.offset(period); switch (Pattern_type) { case 0: // Grid / Trapezoidal @@ -3148,21 +3158,31 @@ bool FillRectilinear::fill_surface_trapezoidal( // Align bounding box to the grid, phased through the box center so separated infills align // each part on itself (grid_center is the origin for a standalone object / feature off). // Captured before the merge, which grows bb and would otherwise shift its center. + BoundingBox bb = this->extended_object_bounding_box(); const Point grid_center = bb.center(); bb.merge(align_to_grid(bb.min, Point(period, period), grid_center)); const coord_t xmin = bb.min.x(); - const coord_t xmax = bb.max.x(); const coord_t ymin = bb.min.y(); - const coord_t ymax = bb.max.y(); + + auto transpose = [&grid_center](const Point &p) { + return Point(grid_center.x() + p.y() - grid_center.y(), grid_center.y() + p.x() - grid_center.x()); + }; + if (infill_layer_id % 2 == 1) + cover = BoundingBox(transpose(cover.min), transpose(cover.max)); + const coord_t row_spacing = period / 2; + const coord_t first_x = xmin + (coord_t(std::floor(double(cover.min.x() - xmin) / period)) - 1) * period; + const coord_t last_x = xmin + (coord_t(std::ceil(double(cover.max.x() - xmin) / period)) + 1) * period; + const coord_t first_row = coord_t(std::floor(double(cover.min.y() - ymin) / row_spacing)) - 1; + const coord_t last_row = coord_t(std::ceil(double(cover.max.y() - ymin) / row_spacing)) + 1; // Create the two base row patterns once Polyline base_row_normal; - base_row_normal.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid + base_row_normal.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid Polyline base_row_flipped; - base_row_flipped.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid + base_row_flipped.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid - // Build complete rows from xmin to xmax - for (coord_t x = xmin; x < xmax; x += period) { + // Build rows on the same global period grid, limited to the surface cover. + for (coord_t x = first_x; x < last_x; x += period) { // Normal row base_row_normal.points.emplace_back(Point(x, d1 / 2)); // P0 base_row_normal.points.emplace_back(Point(x + d1 / 2, d1 / 2)); // P1 @@ -3178,13 +3198,14 @@ bool FillRectilinear::fill_surface_trapezoidal( } // Pre-allocate polylines - const size_t estimated_rows = ((ymax - ymin) / (period / 2) + 1); + const size_t estimated_rows = size_t(last_row - first_row + 1); polylines.reserve(estimated_rows); - bool flip_vertical = false; + bool flip_vertical = (first_row % 2) != 0; - // Now just copy and translate vertically - for (coord_t y = ymin; y < ymax; y += period / 2) { + // Copy and translate only rows intersecting the surface cover. + for (coord_t row = first_row; row <= last_row; ++row) { + const coord_t y = ymin + row * row_spacing; Polyline pl_row = flip_vertical ? base_row_flipped : base_row_normal; // Translate all points vertically @@ -3200,16 +3221,10 @@ bool FillRectilinear::fill_surface_trapezoidal( // Orca: mirror across the diagonal through grid_center (not the origin), so the swapped // layers stay aligned with the center-phased grid. For a standalone object / feature off, // grid_center is the origin and this is a plain x/y swap. - if (infill_layer_id % 2 == 1) { - for (Polyline& pl : polylines) { - for (Point& p : pl.points) { - const coord_t dx = p.x() - grid_center.x(); - const coord_t dy = p.y() - grid_center.y(); - p.x() = grid_center.x() + dy; - p.y() = grid_center.y() + dx; - } - } - } + if (infill_layer_id % 2 == 1) + for (Polyline& pl : polylines) + for (Point& p : pl.points) + p = transpose(p); break; } @@ -3230,40 +3245,24 @@ bool FillRectilinear::fill_surface_trapezoidal( const coord_t d2_tri = coord_t(2.0 / std::sqrt(3.0) * d1); const coord_t h = coord_t(0.5 * std::sqrt(3.0) * period); // height of triangle - // Align bounding box to the grid - bb.merge(align_to_grid(bb.center(), Point(period,h))); - const size_t layer_mod = infill_layer_id % 3; - const double angle = layer_mod * 2.0 * M_PI / 3.0; - - const Point rotation_center = bb.center(); - const coord_t half_w = bb.size().x() / 2; - const coord_t half_h = bb.size().y() / 2; - - // Compute how many full periods fit in each direction - const coord_t num_periods_x = coord_t(std::ceil(half_w / double(period))); - coord_t num_periods_y =coord_t(std::ceil(half_h / double(h))); - // Ensure an even number of rows so the pattern stays centered - if ((num_periods_y % 2) != 0) - ++num_periods_y; - - // Compute aligned limits (symmetric around the origin) - const coord_t x_min_aligned = -num_periods_x * period; - const coord_t x_max_aligned = num_periods_x * period; - const coord_t y_min_aligned = -num_periods_y * h; - const coord_t y_max_aligned = num_periods_y * h; + // Keep the existing origin-anchored lattice, but generate only nearby tiles. + const coord_t x_min_aligned = (coord_t(std::floor(double(cover.min.x()) / period)) - 1) * period; + const coord_t x_max_aligned = (coord_t(std::ceil(double(cover.max.x()) / period)) + 1) * period; + const coord_t first_row = coord_t(std::floor(double(cover.min.y()) / h)) - 1; + const coord_t last_row = coord_t(std::ceil(double(cover.max.y()) / h)) + 1; // Pre-allocate estimated number of polylines - const size_t estimated_rows = (y_max_aligned - y_min_aligned) / h + 2; - const size_t estimated_polylines = (estimated_rows + 1) * 2; // base line + trapezoid line per row + const size_t estimated_rows = size_t(last_row - first_row + 1); + const size_t estimated_polylines = estimated_rows * 2; // base line + trapezoid line per row polylines.reserve(estimated_polylines); // Create the two base row templates once Polyline base_line_template; base_line_template.points.reserve(2); // 2 points for base line Polyline trapezoid_row_normal; - trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid + trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid Polyline trapezoid_row_shifted; - trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid + trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid // Build base line template (from x_min_aligned to x_max_aligned) base_line_template.points.emplace_back(Point(x_min_aligned, 0)); base_line_template.points.emplace_back(Point(x_max_aligned, 0)); @@ -3283,10 +3282,10 @@ bool FillRectilinear::fill_surface_trapezoidal( for (auto& p : trapezoid_row_shifted.points) p.y() = h - p.y(); - bool shift_row = false; - // Generate pattern by copying and translating templates vertically - for (coord_t y = y_min_aligned; y < y_max_aligned; y += h) { + bool shift_row = (first_row % 2) != 0; + for (coord_t row = first_row; row <= last_row; ++row) { + const coord_t y = row * h; // Base line - copy and translate Polyline base_line = base_line_template; for (Point& p : base_line.points) { @@ -3306,12 +3305,6 @@ bool FillRectilinear::fill_surface_trapezoidal( shift_row = !shift_row; } - - // Rotate around origin (0,0) - if (layer_mod) - for (auto& pl : polylines) - pl.rotate(angle, Point(0,0)); - break; } @@ -3325,36 +3318,25 @@ bool FillRectilinear::fill_surface_trapezoidal( const coord_t d1_half_base = d1_half / std::sqrt(3.0); const coord_t half_period = period / 2; const coord_t quarter_period = period / 4; + const coord_t row_y_offset = tri_height - (2 * tri_height) / 3; - bb.merge(align_to_grid(bb.center(), Point(period, tri_height))); - const size_t layer_mod = infill_layer_id % 3; - const double angle = layer_mod * 2.0 * M_PI / 3.0; + // Keep the lattice anchored at the origin while generating only tiles around this surface. + const int64_t first_tile = int64_t(std::floor(double(cover.min.x()) / period)) - 1; + const int64_t last_tile = int64_t(std::ceil(double(cover.max.x()) / period)) + 1; + const int64_t first_row = int64_t(std::floor(double(cover.min.y() - row_y_offset) / hex_height)) - 1; + const int64_t last_row = int64_t(std::ceil(double(cover.max.y() - row_y_offset) / hex_height)) + 1; - const coord_t half_w = bb.size().x() / 2; - const coord_t half_h = bb.size().y() / 2; - - const coord_t num_periods_x = coord_t(std::ceil(half_w / double(period))); - coord_t num_periods_y = coord_t(std::ceil(half_h / double(hex_height))); - if ((num_periods_y % 2) != 0) - ++num_periods_y; - - const coord_t x_alignment_shift = half_period; - const coord_t y_alignment_shift = (2 * tri_height) / 3; - const coord_t x_min_aligned = -num_periods_x * period - x_alignment_shift; - const coord_t x_max_aligned = num_periods_x * period - x_alignment_shift; - const coord_t y_min_aligned = -num_periods_y * hex_height - y_alignment_shift; - const coord_t y_max_aligned = num_periods_y * hex_height - y_alignment_shift; - - const size_t estimated_rows = (y_max_aligned - y_min_aligned) / hex_height + 2; - const size_t estimated_polylines = (estimated_rows + 1) * 2; + const size_t estimated_rows = size_t(last_row - first_row + 1); + const size_t estimated_polylines = estimated_rows * 2; polylines.reserve(estimated_polylines); Polyline star_row_normal; - star_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7); + star_row_normal.points.reserve(size_t(last_tile - first_tile) * 7); Polyline star_row_mirrored; - star_row_mirrored.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7); + star_row_mirrored.points.reserve(size_t(last_tile - first_tile) * 7); - for (coord_t x = x_min_aligned; x < x_max_aligned; x += period) { + for (int64_t tile = first_tile; tile < last_tile; ++tile) { + const coord_t x = coord_t(tile * period) - half_period; star_row_normal.points.emplace_back(Point(x, hex_height)); // P0 star_row_normal.points.emplace_back(Point(x + quarter_period - d1, hex_height)); // P1 star_row_normal.points.emplace_back(Point(x + quarter_period + d1_half, hex_height - chamfer_height)); // P2 @@ -3368,9 +3350,7 @@ bool FillRectilinear::fill_surface_trapezoidal( for (auto& p : star_row_mirrored.points) p.y() = hex_height - p.y(); - size_t pair_idx = 0; const coord_t global_x_shift = half_period; - const coord_t global_y_shift = tri_height; auto append_row_with_shift = [&polylines](const Polyline& row_template, coord_t x_shift, coord_t y_shift) { Polyline row = row_template; for (Point& p : row.points) { @@ -3381,16 +3361,12 @@ bool FillRectilinear::fill_surface_trapezoidal( polylines.emplace_back(std::move(row)); }; - for (coord_t y = y_min_aligned; y < y_max_aligned; y += hex_height, ++pair_idx) { - const coord_t x_shift = (pair_idx % 2 == 0) ? 0 : half_period; - append_row_with_shift(star_row_normal, x_shift + global_x_shift, y + global_y_shift); - append_row_with_shift(star_row_mirrored, x_shift + global_x_shift, y + global_y_shift); + for (int64_t row = first_row; row <= last_row; ++row) { + const coord_t y = coord_t(row * hex_height) + row_y_offset; + const coord_t x_shift = (row % 2 == 0) ? 0 : half_period; + append_row_with_shift(star_row_normal, x_shift + global_x_shift, y); + append_row_with_shift(star_row_mirrored, x_shift + global_x_shift, y); } - - if (layer_mod) - for (auto& pl : polylines) - pl.rotate(angle, Point(0, 0)); - break; } @@ -3407,19 +3383,9 @@ bool FillRectilinear::fill_surface_trapezoidal( for (Vec2d &p : levels.front()) p.y() = h - p.y(); - const size_t layer_mod = infill_layer_id % 3; - const double angle = layer_mod * 2.0 * M_PI / 3.0; - - // Only cover the surface, seen in the frame the pattern is built in. - ExPolygon local = expolygon; - local.translate(-rotate_vector.second.x(), -rotate_vector.second.y()); - if (layer_mod) - local.rotate(-angle); - BoundingBox cover = get_extents(local); - cover.offset(period); - const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1; const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1; + polylines.reserve(size_t(n_max - n_min + 1) * levels.size()); for (int64_t n = n_min; n <= n_max; ++n) { const double x_off = (n & 1) ? 0.5 * period : 0.; const double base = y0 + double(n) * h - tau; @@ -3435,11 +3401,6 @@ bool FillRectilinear::fill_surface_trapezoidal( polylines.emplace_back(std::move(row)); } } - - if (layer_mod) - for (Polyline &pl : polylines) - pl.rotate(angle, Point(0, 0)); - break; } @@ -3448,21 +3409,38 @@ bool FillRectilinear::fill_surface_trapezoidal( break; } - // Orca: cases 1 & 2 build the pattern symmetrically around the origin, so on their own they - // phase to the global origin and every part shares one grid. Shift the pattern onto the box - // center this->bounding_box carries, so separated infills align each part on itself. The center - // is the origin for a standalone object (or when the feature is off), making this a no-op there. + // Orca: cases 1 to 3 anchor the pattern at the origin, so on their own they phase to the global + // origin and every part shares one grid. Shift the pattern onto the box center + // this->bounding_box carries, so separated infills align each part on itself. The center is the + // origin for a standalone object (or when the feature is off), making the shift a no-op there. if (Pattern_type != 0) - for (Polyline &pl : polylines) + for (Polyline &pl : polylines) { + if (layer_mod) + pl.rotate(angle, Point(0, 0)); pl.translate(rotate_vector.second); + } // Orca: round the corners of the trapezoids. The straight base lines of the triangular family // have no corner to round. smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Only the centerlines within d1 / 2 of the surface have outlines reaching it. + polylines = intersection_pl(std::move(polylines), offset(expolygon, float(d1 / 2))); + // Apply multiline fill multiline_fill(polylines, params, spacing); + // Start each outline on the cap at the first end of its path, outside the surface, so clipping splits it only there. + const Vec2d row_dir = Pattern_type != 0 ? Vec2d(std::cos(angle), std::sin(angle)) : infill_layer_id % 2 ? Vec2d::UnitY() : Vec2d::UnitX(); + for (Polyline &pl : polylines) + if (pl.size() > 3 && pl.first_point() == pl.last_point()) { + pl.points.pop_back(); + std::rotate(pl.points.begin(), std::min_element(pl.points.begin(), pl.points.end(), [&row_dir](const Point &a, const Point &b) { + return row_dir.dot(a.cast()) < row_dir.dot(b.cast()); + }), pl.points.end()); + pl.points.emplace_back(pl.points.front()); + } + // Contract surface polygon by half line width to avoid excesive overlap with perimeter ExPolygons contracted = offset_ex(expolygon, -float(scale_(0.5 * this->spacing))); @@ -3545,9 +3523,7 @@ Polylines FillGrid::fill_surface(const Surface *surface, const FillParams ¶m if (params.multiline > 1) { // Experimental trapezoidal grid if (!this->fill_surface_trapezoidal( - surface, params, - { { 0.f, 0.f }, { float(M_PI / 2.), 0.f } }, - polylines_out,0)) + surface, params, polylines_out, 0)) BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed."; } else { @@ -3587,9 +3563,7 @@ Polylines FillTriangles::fill_surface(const Surface *surface, const FillParams & if (params.multiline > 1) { // Experimental trapezoidal grid if (!this->fill_surface_trapezoidal( - surface, params, - { { 0.f, 0.f }, { float(M_PI / 2.), 0.f } }, - polylines_out,1)) + surface, params, polylines_out, 1)) BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed."; } else { @@ -3608,9 +3582,7 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams ¶ Polylines polylines_out; if (params.multiline > 1) { if (!this->fill_surface_trapezoidal( - surface, params, - {{0.f, 0.f}, {float(M_PI / 3.), 0.f}, {float(2. * M_PI / 3.), float((3. / 2.) * this->spacing * params.multiline / params.density)}}, - polylines_out, 2)) + surface, params, polylines_out, 2)) BOOST_LOG_TRIVIAL(error) << "FillStars::fill_surface_trapezoidal() failed."; } else { if (! this->fill_surface_by_multilines( @@ -3626,7 +3598,7 @@ Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ { Polylines polylines_out; if (params.multiline > 1) { - if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3)) + if (!this->fill_surface_trapezoidal(surface, params, polylines_out, 3)) BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed."; return polylines_out; } diff --git a/src/libslic3r/Fill/FillRectilinear.hpp b/src/libslic3r/Fill/FillRectilinear.hpp index 126cbe3a15..adb9ef5131 100644 --- a/src/libslic3r/Fill/FillRectilinear.hpp +++ b/src/libslic3r/Fill/FillRectilinear.hpp @@ -29,7 +29,7 @@ protected: float pattern_shift; }; bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list &sweep_params, Polylines &polylines_out); - bool fill_surface_trapezoidal(const Surface *surface, FillParams params, const std::initializer_list &sweep_params, Polylines &polylines_out,int Pattern_type); + bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type); // The extended bounding box of the whole object that covers any rotation of every layer. BoundingBox extended_object_bounding_box() const; diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 0ff7e65e5d..789ab84457 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -1199,6 +1199,64 @@ TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one li REQUIRE(single_smooth.length == single_sharp.length); } +TEST_CASE("Multiline infill of an object matches the infill of a larger object with the same center", "[Fill]") +{ + const InfillPattern pattern = GENERATE(ipGrid, ipTriangles, ipStars, ipCubic); + const int multiline = GENERATE(2, 3); + // A square with cells as large as itself, whose corners are as far out as the object bounding box + // reaches, and a strip with small cells, whose extents change with every layer orientation. + const auto [half, density] = GENERATE(table({ { Vec2d(20., 20.), 0.15f }, { Vec2d(60., 4.), 0.35f } })); + CAPTURE(pattern, multiline, half.x(), half.y(), density); + + // Off the origin; the same center gives both objects the same pattern. + auto rectangle = [](const Vec2d &half) { + const Vec2d center(100., 60.); + return ExPolygon{ Points{ Point::new_scale(center.x() - half.x(), center.y() - half.y()), + Point::new_scale(center.x() + half.x(), center.y() - half.y()), + Point::new_scale(center.x() + half.x(), center.y() + half.y()), + Point::new_scale(center.x() - half.x(), center.y() + half.y()) } }; + }; + const ExPolygon object = rectangle(half); + const ExPolygon larger = rectangle(half + Vec2d(10., 10.)); + auto fill = [pattern, multiline, density = density](const ExPolygon ®ion, size_t layer_id) { + std::unique_ptr filler(Fill::new_from_type(pattern)); + filler->spacing = 0.45; + filler->angle = float(M_PI / 7.); + filler->fixed_angle = true; + filler->layer_id = layer_id; + filler->z = 0.2 * double(layer_id + 1); + filler->set_bounding_box(get_extents(region.contour)); + + FillParams params; + params.density = density; + params.multiline = multiline; + params.dont_adjust = true; + Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + // Away from the boundary of the object, where both are clipped and connected the same way. + const Polygons inner = shrink(to_polygons(object), scale_(1.)); + auto farthest = [&inner](const Polylines &from, const Polylines &to) { + const AABBTreeLines::LinesDistancer tree(to_lines(to)); + double distance = 0.; + for (const Polyline &path : intersection_pl(from, inner)) + for (const Point &point : path.equally_spaced_points(scale_(0.2))) + distance = std::max(distance, tree.distance_from_lines(point)); + return unscale(distance); + }; + + // Both layer orientations of Grid, all three of the triangular family. + for (size_t layer_id = 0; layer_id < 3; ++layer_id) { + CAPTURE(layer_id); + const Polylines walls = fill(object, layer_id); + REQUIRE_FALSE(walls.empty()); + CHECK(get_intersections(to_lines(walls)).empty()); + const Polylines reference = fill(larger, layer_id); + CHECK(farthest(reference, walls) < 0.01); + CHECK(farthest(walls, reference) < 0.01); + } +} + TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]") { const int multiline = GENERATE(2, 3);