diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp index c740dd2b4b..c9a108573d 100644 --- a/src/libslic3r/Fill/FillGyroid.cpp +++ b/src/libslic3r/Fill/FillGyroid.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "FillBase.hpp" #include "FillGyroid.hpp" @@ -123,44 +124,41 @@ static inline double f(double x, double z_sin, double z_cos, bool vertical, bool } } +// Repeats one period of a wave from the last sample at or before x_min to the first one at or after x_max. static inline Polyline make_wave( - const std::vector& one_period, double width, double height, double offset, double scaleFactor, - double z_cos, double z_sin, bool vertical, bool flip) + const std::vector& one_period, double x_min, double x_max, double offset, double scaleFactor, bool vertical) { - std::vector points = one_period; - double period = points.back()(0); - if (width != period) // do not extend if already truncated - { - points.reserve(one_period.size() * size_t(floor(width / period))); - points.pop_back(); + const double period = one_period.back().x(); + // The last sample of a period is the first one of the next. + const size_t n = one_period.size() - 1; + double x0 = std::floor(x_min / period) * period; + size_t i = 0; + while (i + 1 < n && x0 + one_period[i + 1].x() <= x_min) + ++i; - size_t n = points.size(); - do { - points.emplace_back(points[points.size()-n].x() + period, points[points.size()-n].y()); - } while (points.back()(0) < width - EPSILON); - - points.emplace_back(Vec2d(width, f(width, z_sin, z_cos, vertical, flip))); - } - - // and construct the final polyline to return: Polyline polyline; - polyline.points.reserve(points.size()); - for (auto& point : points) { - point(1) += offset; - point(1) = std::clamp(double(point.y()), 0., height); + polyline.points.reserve(size_t((x_max - x0) / period + 1.) * n + 1); + for (;;) { + Vec2d point(x0 + one_period[i].x(), one_period[i].y() + offset); + const bool last = point.x() >= x_max; if (vertical) std::swap(point(0), point(1)); polyline.points.emplace_back((point * scaleFactor).cast()); + if (last) + break; + if (++i == n) { + i = 0; + x0 += period; + } } - return polyline; } -static std::vector make_one_period(double width, double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance) +static std::vector make_one_period(double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance) { std::vector points; double dx = M_PI_2; // exact coordinates on main inflexion lobes - double limit = std::min(2*M_PI, width); + double limit = 2*M_PI; points.reserve(coord_t(ceil(limit / tolerance / 3))); for (double x = 0.; x < limit - EPSILON; x += dx) { @@ -239,7 +237,8 @@ static inline double compute_omega_factor(double density_adjusted, double line_s return std::clamp(raw, 1.0, 2.0); } -static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height) +// Waves covering bbox, with the pattern anchored at origin. +static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, const BoundingBox &bbox, const Point &origin) { const double scaleFactor = scale_(line_spacing) / density_adjusted; @@ -254,29 +253,36 @@ static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double const double z_cos = cos(z); bool vertical = (std::abs(z_sin) <= std::abs(z_cos)); + // Range to cover in pattern units, with the waves running along x. + Vec2d lo = (bbox.min - origin).cast() / scaleFactor; + Vec2d hi = (bbox.max - origin).cast() / scaleFactor; double lower_bound = 0.; - double upper_bound = height; bool flip = true; if (vertical) { flip = false; lower_bound = -M_PI; - upper_bound = width - M_PI_2; - std::swap(width,height); + std::swap(lo(0), lo(1)); + std::swap(hi(0), hi(1)); } - std::vector one_period_odd = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // creates one period of the waves, so it doesn't have to be recalculated all the time + std::vector one_period_odd = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // creates one period of the waves, so it doesn't have to be recalculated all the time flip = !flip; // even polylines are a bit shifted - std::vector one_period_even = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance); - Polylines result; + std::vector one_period_even = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); - for (double y0 = lower_bound; y0 < upper_bound + EPSILON; y0 += M_PI) { - // creates odd polylines - result.emplace_back(make_wave(one_period_odd, width, height, y0, scaleFactor, z_cos, z_sin, vertical, flip)); - // creates even polylines - y0 += M_PI; - if (y0 < upper_bound + EPSILON) { - result.emplace_back(make_wave(one_period_even, width, height, y0, scaleFactor, z_cos, z_sin, vertical, flip)); + // Every wave spans [offset + f_min, offset + f_max] across. + double f_min = std::numeric_limits::max(); + double f_max = std::numeric_limits::lowest(); + for (const std::vector *one_period : { &one_period_odd, &one_period_even }) + for (const Vec2d &point : *one_period) { + f_min = std::min(f_min, point.y()); + f_max = std::max(f_max, point.y()); } + + Polylines result; + for (int i = int(std::ceil((lo.y() - f_max - lower_bound) / M_PI)); lower_bound + i * M_PI + f_min <= hi.y(); ++i) { + Polyline &wave = result.emplace_back(make_wave(i % 2 == 0 ? one_period_odd : one_period_even, lo.x(), hi.x(), + lower_bound + i * M_PI, scaleFactor, vertical)); + wave.translate(origin); } return result; @@ -302,12 +308,12 @@ void FillGyroid::_fill_surface_single( // Distance between the gyroid waves in scaled coordinates. coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); - // align bounding box to a multiple of our grid module - bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance))); + // Anchor the pattern to our grid module; the 10-line shift keeps its established phase. + const coord_t shift = coord_t(10 * scale_(this->spacing)); + const Point origin = align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)) - Point(shift, shift); - // Expand the bounding box to avoid artifacts at the edges - coord_t expand = 10 * (scale_(this->spacing)); - bb.offset(expand); + // Keep the pattern ends and the multiline copies outside the contour. + bb.offset(scale_(this->spacing * params.multiline)); // generate pattern Polylines polylines; @@ -327,23 +333,14 @@ void FillGyroid::_fill_surface_single( const float density_factor = std::max(0.001f, float(params.density * DensityAdjust / params.multiline)); const float period = float(2.0 * M_PI) * float(this->spacing) / density_factor; - // bb is already expanded above by 10 * scale_(spacing) for edge artifacts; - // skip a second offset here to avoid raster-area bloat in the marching squares pass. + // A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region. + const coord_t cell = scaled(marchsq::GyroidField::gsizef); + bb.offset(cell); + bb.merge(align_to_grid(bb.min, Point(cell, cell))); marchsq::GyroidField sf(bb, this->z, period, float(omega)); polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION); } else { - polylines = make_gyroid_waves( - scale_(this->z), - density_adjusted, - this->spacing, - ceil(bb.size()(0) / distance) + 1., - ceil(bb.size()(1) / distance) + 1.); - - // The parametric generator produces wave coords relative to the grid origin; - // shift them into absolute layer coords. The marching-squares branch above - // already emits absolute coords via GyroidField::to_Point, so it skips this. - for (Polyline &pl : polylines) - pl.translate(bb.min); + polylines = make_gyroid_waves(scale_(this->z), density_adjusted, this->spacing, bb, origin); } // Apply multiline offset if needed diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 789ab84457..7680206dc8 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -13,6 +13,7 @@ #include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/FillAdaptive.hpp" +#include "libslic3r/Fill/FillGyroid.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/IntersectionPoints.hpp" @@ -1257,6 +1258,61 @@ TEST_CASE("Multiline infill of an object matches the infill of a larger object w } } +TEST_CASE("Gyroid infill of an object matches the infill of a larger object with the same center", "[Fill]") +{ + const bool optimized = GENERATE(false, true); + const int multiline = GENERATE(1, 2); + const float density = GENERATE(0.05f, 0.2f); + const double spacing = 0.45; + CAPTURE(optimized, multiline, density); + + auto circle = [](double radius) { + Polygon contour = make_circle_num_segments(scale_(radius), 120); + contour.translate(Point::new_scale(100., 60.)); + return ExPolygon(std::move(contour)); + }; + const ExPolygon object = circle(20.); + const ExPolygon larger = circle(30.); + auto fill = [optimized, multiline, density, spacing](const ExPolygon ®ion, double z) { + std::unique_ptr filler(Fill::new_from_type(ipGyroid)); + filler->spacing = spacing; + filler->angle = float(M_PI / 7.); + filler->z = z; + + FillParams params; + params.density = density; + params.multiline = multiline; + params.gyroid_optimized = optimized; + 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); + }; + + // Marching squares simplifies rings that start elsewhere in each object. + const double tolerance = optimized ? SPARSE_INFILL_RESOLUTION + 0.01 : 0.01; + // Half a z period of the waves, through both switches between horizontal and vertical waves. + const double wave_distance = spacing * multiline / (density * FillGyroid::DensityAdjust); + for (int step = 0; step <= 8; ++step) { + const double z = wave_distance * M_PI * step / 8.; + CAPTURE(z); + const Polylines paths = fill(object, z); + REQUIRE_FALSE(paths.empty()); + const Polylines reference = fill(larger, z); + CHECK(farthest(reference, paths) < tolerance); + CHECK(farthest(paths, reference) < tolerance); + } +} + TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]") { const int multiline = GENERATE(2, 3);