#include "../ClipperUtils.hpp" #include "../MarchingSquares.hpp" #include #include #include #include #include #include "libslic3r/BoundingBox.hpp" #include #include "libslic3r/Execution/ExecutionTBB.hpp" #include #include #include "libslic3r/ExPolygon.hpp" #include "FillBase.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/libslic3r.h" #include "libslic3r/Polyline.hpp" #include "FillGyroid.hpp" #include "libslic3r/Polygon.hpp" // --------------------------------------------------------------------------- // Marching-squares scalar field for the optimized gyroid branch. // Modeled after FillTpmsFK.cpp's ScalarField. // // The gyroid scalar field is the standard implicit equation // F(x,y,z) = sin(fx*x)cos(fy*y) + sin(fy*y)cos(fz*z) + sin(fz*z)cos(fx*x) // Marching squares extracts the iso-zero contour, which gives smoother // transitions between vertical and horizontal regimes than the analytical // asin-based wave generator. Setting fz = omega * baseline anisotropically // tightens the wave along the layer-stacking axis, shortening the effective // vertical strand length and improving column-buckling resistance under // Z-axis compression. // --------------------------------------------------------------------------- namespace marchsq { using namespace Slic3r; using coordr_t = long; using Pointf = Vec2d; struct GyroidField { static constexpr float gsizef = 0.40f; static constexpr float rsizef = 0.004f; const coord_t rsize = scaled(rsizef); const coordr_t gsize = std::round(gsizef / rsizef); Point size; Point offs; coordf_t z; float fx; float fy; float fz; float isoval = 0.0f; explicit GyroidField(const BoundingBox bb, const coordf_t z, const float period, const float omega = 1.0f) : size{bb.size()}, offs{bb.min}, z{z} { const float baseline = float(2.0 * PI) / std::max(period, 1e-3f); fx = baseline; fy = baseline; fz = omega * baseline; } float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const { const float a = fx * float(x); const float b = fy * float(y); const float c = fz * float(z_arg); return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a); } float get_scalar(Coord p) const { Pointf pf = to_Pointf(p); return get_scalar(pf.x(), pf.y(), z); } inline coord_t to_coord (const coordr_t& x) const { return x * rsize; } inline coordr_t to_coordr(const coord_t& x) const { return x / rsize; } inline Point to_Point (const Coord& p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); } inline Coord to_Coord (const Point& p) const { return Coord(to_coordr(p.y() - offs.y()), to_coordr(p.x() - offs.x())); } inline Pointf to_Pointf(const Point& p) const { return Pointf(unscaled(p.x()), unscaled(p.y())); } inline Pointf to_Pointf(const Coord& p) const { return to_Pointf(to_Point(p)); } }; template<> struct _RasterTraits { using ValueType = float; static float get (const GyroidField& sf, size_t row, size_t col) { return sf.get_scalar(Coord(row, col)); } static size_t rows(const GyroidField& sf) { return sf.to_coordr(sf.size.y()); } static size_t cols(const GyroidField& sf) { return sf.to_coordr(sf.size.x()); } }; inline Polylines get_gyroid_polylines(const GyroidField& sf, const double tolerance = SCALED_EPSILON) { std::vector rings = execute_with_policy(ex_tbb, sf, sf.isoval, {sf.gsize, sf.gsize}); Polylines polys; polys.reserve(rings.size()); for (const Ring& ring : rings) { Polyline poly; Points& pts = poly.points; pts.reserve(ring.size() + 1); for (const Coord& crd : ring) pts.emplace_back(sf.to_Point(crd)); pts.push_back(pts.front()); if (tolerance >= 0.0) poly.simplify(tolerance); polys.emplace_back(poly); } return polys; } } // namespace marchsq namespace Slic3r { static inline double f(double x, double z_sin, double z_cos, bool vertical, bool flip) { if (vertical) { double phase_offset = (z_cos < 0 ? M_PI : 0) + M_PI; double a = sin(x + phase_offset); double b = - z_cos; double res = z_sin * cos(x + phase_offset + (flip ? M_PI : 0.)); double r = sqrt(sqr(a) + sqr(b)); return asin(a/r) + asin(res/r) + M_PI; } else { double phase_offset = z_sin < 0 ? M_PI : 0.; double a = cos(x + phase_offset); double b = - z_sin; double res = z_cos * sin(x + phase_offset + (flip ? 0 : M_PI)); double r = sqrt(sqr(a) + sqr(b)); return (asin(a/r) + asin(res/r) + 0.5 * M_PI); } } // 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 x_min, double x_max, double offset, double scaleFactor, bool vertical) { 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; Polyline polyline; 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 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 = 2*M_PI; points.reserve(coord_t(ceil(limit / tolerance / 3))); for (double x = 0.; x < limit - EPSILON; x += dx) { points.emplace_back(Vec2d(x, f(x, z_sin, z_cos, vertical, flip))); } points.emplace_back(Vec2d(limit, f(limit, z_sin, z_cos, vertical, flip))); // piecewise increase in resolution up to requested tolerance for(;;) { size_t size = points.size(); for (unsigned int i = 1;i < size; ++i) { auto& lp = points[i-1]; // left point auto& rp = points[i]; // right point double x = lp(0) + (rp(0) - lp(0)) / 2; double y = f(x, z_sin, z_cos, vertical, flip); Vec2d ip = {x, y}; if (std::abs(cross2(Vec2d(ip - lp), Vec2d(ip - rp))) > sqr(tolerance)) { points.emplace_back(std::move(ip)); } } if (size == points.size()) break; else { // insert new points in order std::sort(points.begin(), points.end(), [](const Vec2d &lhs, const Vec2d &rhs) { return lhs(0) < rhs(0); }); } } return points; } // --------------------------------------------------------------------------- // "Optimized" gyroid wave: marching-squares variant gated on // params.gyroid_optimized. The wave shape is extracted from the gyroid // implicit scalar field (see marchsq::GyroidField above) at iso=0, with // the Z dimension's spatial frequency multiplied by an Euler-Bernoulli // buckling-derived factor so the vertical strands become shorter columns, // raising the critical buckling load against Z-axis compression. // // The formula is INVERTED from a naive "scale with density" derivation: // at LOW density the gyroid strands are long and slender (prime buckling // targets), so they need the most shortening; at high density the strands // are already short and need little extra help. omega is therefore the // inverse-square-root of density_adjusted: // // omega = sqrt(1 / density_adj) / sqrt(1 + layer_h/spacing), // clamped [1.0, 2.0] // // fx and fy are left at the baseline frequency, so the per-XY-slice line // length per unit area is preserved -> mass at the same `sparse_infill_density` // setting matches the standard gyroid path. Strength gain comes purely from // the shorter vertical column length (P_cr proportional to 1/L^2). // // Empirical Python sim (sim_gyroid_compare.py) at layer_h=0.20, spacing=0.45: // // density omega line/std strength/std strength_per_mass // 10% 2.00 1.00 2.84 2.84 // 15% 1.38 1.00 1.89 1.89 // 20% 1.19 1.00 1.42 1.42 // 30% 1.00 1.00 1.00 1.00 // 50%+ 1.00 1.00 1.00 1.00 // // When gyroid_optimized is false, behavior is byte-identical to the // standard parametric gyroid path below. // --------------------------------------------------------------------------- static inline double compute_omega_factor(double density_adjusted, double line_spacing, double layer_height) { double lh_ratio = (line_spacing > 0.) ? layer_height / line_spacing : 0.5; double correction = 1.0 / std::sqrt(1.0 + lh_ratio); double raw = std::sqrt(1.0 / std::max(density_adjusted, 0.1)) * correction; return std::clamp(raw, 1.0, 2.0); } // 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; // tolerance in scaled units. clamp the maximum tolerance as there's // no processing-speed benefit to do so beyond a certain point const double tolerance = std::min(line_spacing / 2, FillGyroid::PatternTolerance) / unscale(scaleFactor); //scale factor for 5% : 8 712 388 // 1z = 10^-6 mm ? const double z = gridZ / scaleFactor; const double z_sin = sin(z); 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.; bool flip = true; if (vertical) { flip = false; lower_bound = -M_PI; std::swap(lo(0), lo(1)); std::swap(hi(0), hi(1)); } 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(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // 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; } // FIXME: needed to fix build on Mac on buildserver constexpr double FillGyroid::PatternTolerance; void FillGyroid::_fill_surface_single( const FillParams ¶ms, unsigned int thickness_layers, const std::pair &direction, ExPolygon expolygon, Polylines &polylines_out) { auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.); if(std::abs(infill_angle) >= EPSILON) expolygon.rotate(-infill_angle); BoundingBox bb = expolygon.contour.bounding_box(); // Density adjusted to have a good %of weight. double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline); // Distance between the gyroid waves in scaled coordinates. coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); // 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); // Keep the pattern ends and the multiline copies outside the contour. bb.offset(scale_(this->spacing * params.multiline)); // generate pattern Polylines polylines; if (params.gyroid_optimized) { // Marching-squares path on the gyroid implicit field. Base period matches // the standard parametric path's wavelength: 2*pi * spacing / density_adj. // omega >= 1 always, so fz >= baseline -> shorter vertical wavelength -> // shorter effective column length -> higher buckling resistance. // // Mass: fx and fy are left at baseline (same as standard), so the // per-XY-slice line length per unit area is approximately preserved. // Empirically (sim_gyroid_compare.py) the optimized line/std ratio is // ~1.000 across densities, so no period compensation is needed. const double lh = (params.layer_height > 0.) ? double(params.layer_height) : double(this->spacing); const double omega = compute_omega_factor(density_adjusted, this->spacing * params.multiline, lh); 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; // 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, bb, origin); } // Apply multiline offset if needed multiline_fill(polylines, params, spacing); polylines = intersection_pl(std::move(polylines), expolygon); if (! polylines.empty()) { // Remove very small bits, but be careful to not remove infill lines connecting thin walls! // The infill perimeter lines should be separated by around a single infill line width. const double minlength = scale_(0.8 * this->spacing); polylines.erase( std::remove_if(polylines.begin(), polylines.end(), [minlength](const Polyline &pl) { return pl.length() < minlength; }), polylines.end()); } if (! polylines.empty()) { // connect lines size_t polylines_out_first_idx = polylines_out.size(); chain_or_connect_infill(std::move(polylines), expolygon, polylines_out, this->spacing, params); // new paths must be rotated back if (std::abs(infill_angle) >= EPSILON) { for (auto it = polylines_out.begin() + polylines_out_first_idx; it != polylines_out.end(); ++ it) it->rotate(infill_angle); } } } } // namespace Slic3r