mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-30 05:17:19 +00:00
Feature: Smooth Factor for the Hilbert Curve sparse infill (#14969)
This commit is contained in:
@@ -278,6 +278,9 @@ struct SurfaceFillParams
|
|||||||
// For Gyroid: when true, use the parameterized "optimized" wave.
|
// For Gyroid: when true, use the parameterized "optimized" wave.
|
||||||
bool gyroid_optimized = false;
|
bool gyroid_optimized = false;
|
||||||
|
|
||||||
|
// Orca: corner smoothing factor in the range [0, 1].
|
||||||
|
double smooth_factor { 0. };
|
||||||
|
|
||||||
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
|
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
|
||||||
bool separated_infills{false};
|
bool separated_infills{false};
|
||||||
|
|
||||||
@@ -316,6 +319,7 @@ struct SurfaceFillParams
|
|||||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||||
|
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||||
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
|
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
|
||||||
@@ -348,6 +352,7 @@ struct SurfaceFillParams
|
|||||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||||
this->separated_infills == rhs.separated_infills &&
|
this->separated_infills == rhs.separated_infills &&
|
||||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||||
|
this->smooth_factor == rhs.smooth_factor &&
|
||||||
this->fill_order == rhs.fill_order;
|
this->fill_order == rhs.fill_order;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -964,6 +969,11 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
|||||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||||
region_config.sparse_infill_rotate_template.value);
|
region_config.sparse_infill_rotate_template.value);
|
||||||
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
|
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
|
||||||
|
|
||||||
|
// Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill.
|
||||||
|
// FillHilbertCurve::generate clamps and validates the value itself.
|
||||||
|
if (params.pattern == ipHilbertCurve)
|
||||||
|
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
|
||||||
} else {
|
} else {
|
||||||
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
|
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
|
||||||
const bool bottom_layer_direction_set = surface.is_bottom() && region_config.bottom_layer_direction.value >= 0.;
|
const bool bottom_layer_direction_set = surface.is_bottom() && region_config.bottom_layer_direction.value >= 0.;
|
||||||
@@ -1328,6 +1338,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||||
|
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
params.flow = surface_fill.params.flow;
|
params.flow = surface_fill.params.flow;
|
||||||
@@ -1569,6 +1580,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
|||||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||||
params.multiline = surface_fill.params.multiline;
|
params.multiline = surface_fill.params.multiline;
|
||||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||||
|
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||||
|
|
||||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ struct FillParams
|
|||||||
// For Gyroid: when true, use the parameterized "optimized" variant.
|
// For Gyroid: when true, use the parameterized "optimized" variant.
|
||||||
bool gyroid_optimized { false };
|
bool gyroid_optimized { false };
|
||||||
|
|
||||||
|
// Orca: corner smoothing factor in the range [0, 1].
|
||||||
|
double smooth_factor { 0. };
|
||||||
|
|
||||||
// For Lateral lattice
|
// For Lateral lattice
|
||||||
coordf_t lateral_lattice_angle_1 { 0.f };
|
coordf_t lateral_lattice_angle_1 { 0.f };
|
||||||
coordf_t lateral_lattice_angle_2 { 0.f };
|
coordf_t lateral_lattice_angle_2 { 0.f };
|
||||||
|
|||||||
@@ -114,12 +114,12 @@ void FillPlanePath::_fill_surface_single(
|
|||||||
// Filling in a bounding box over the whole object, clip generated polyline against the snug bounding box.
|
// Filling in a bounding box over the whole object, clip generated polyline against the snug bounding box.
|
||||||
snug_bounding_box.translate(-shift.x(), -shift.y());
|
snug_bounding_box.translate(-shift.x(), -shift.y());
|
||||||
InfillPolylineClipper output(snug_bounding_box, distance_between_lines);
|
InfillPolylineClipper output(snug_bounding_box, distance_between_lines);
|
||||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
|
||||||
polyline.points = std::move(output.result());
|
polyline.points = std::move(output.result());
|
||||||
} else {
|
} else {
|
||||||
// Filling in a snug bounding box, no need to clip.
|
// Filling in a snug bounding box, no need to clip.
|
||||||
InfillPolylineOutput output(distance_between_lines);
|
InfillPolylineOutput output(distance_between_lines);
|
||||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
|
||||||
polyline.points = std::move(output.result());
|
polyline.points = std::move(output.result());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +288,147 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using QuinticBezier = std::array<Vec2d, 6>;
|
||||||
|
|
||||||
|
static bool is_bezier_flat(const QuinticBezier &curve, const double deviation)
|
||||||
|
{
|
||||||
|
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
|
||||||
|
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
|
||||||
|
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
|
||||||
|
// comparing squared values avoids a square root.
|
||||||
|
const Vec2d chord = curve.back() - curve.front();
|
||||||
|
const double chord_length_sq = chord.squaredNorm();
|
||||||
|
const double max_cross_sq = deviation * deviation * chord_length_sq;
|
||||||
|
|
||||||
|
for (size_t i = 1; i + 1 < curve.size(); ++i) {
|
||||||
|
const Vec2d offset = curve[i] - curve.front();
|
||||||
|
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
|
||||||
|
if (cross * cross > max_cross_sq)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
|
||||||
|
{
|
||||||
|
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
|
||||||
|
// control point to the left half and one to the right half; the latter is filled backwards to keep
|
||||||
|
// both resulting control polygons in their original parameter direction.
|
||||||
|
QuinticBezier subdivision = curve;
|
||||||
|
left.front() = subdivision.front();
|
||||||
|
right.back() = subdivision.back();
|
||||||
|
for (size_t level = 1; level < curve.size(); ++level) {
|
||||||
|
for (size_t i = 0; i + level < curve.size(); ++i)
|
||||||
|
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
|
||||||
|
left[level] = subdivision.front();
|
||||||
|
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector<Vec2d> &output)
|
||||||
|
{
|
||||||
|
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
|
||||||
|
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
|
||||||
|
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
|
||||||
|
static constexpr size_t max_depth = 16;
|
||||||
|
|
||||||
|
std::vector<QuinticBezier> subcurves(2);
|
||||||
|
subdivide_bezier(curve, subcurves[0], subcurves[1]);
|
||||||
|
|
||||||
|
for (size_t depth = 1; depth < max_depth; ++depth) {
|
||||||
|
bool all_flat = true;
|
||||||
|
for (const QuinticBezier &c : subcurves)
|
||||||
|
if (!is_bezier_flat(c, deviation)) {
|
||||||
|
all_flat = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (all_flat)
|
||||||
|
break;
|
||||||
|
std::vector<QuinticBezier> finer(subcurves.size() * 2);
|
||||||
|
for (size_t i = 0; i < subcurves.size(); ++i)
|
||||||
|
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
|
||||||
|
subcurves = std::move(finer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The curve start is deliberately omitted so consecutive curve pieces can share it without duplication.
|
||||||
|
output.reserve(output.size() + subcurves.size());
|
||||||
|
for (const QuinticBezier &c : subcurves)
|
||||||
|
output.emplace_back(c.back());
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Output>
|
||||||
|
static void generate_smooth_hilbert_curve(
|
||||||
|
coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||||
|
const double corner_distance, Output &output)
|
||||||
|
{
|
||||||
|
// A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed
|
||||||
|
// generator, expand the larger requested dimension to the next valid Hilbert grid size. The output
|
||||||
|
// clipper or the later region intersection removes the padded part of the traversal.
|
||||||
|
size_t sz = 2;
|
||||||
|
const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y);
|
||||||
|
while (sz < sz0)
|
||||||
|
sz <<= 1;
|
||||||
|
|
||||||
|
const size_t point_count = sz * sz;
|
||||||
|
output.reserve(point_count);
|
||||||
|
|
||||||
|
// The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance
|
||||||
|
// if this helper is invoked with an invalid resolution.
|
||||||
|
const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON;
|
||||||
|
// Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance).
|
||||||
|
// At each end, the first three control points are collinear and equally spaced: the tangent follows
|
||||||
|
// the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore
|
||||||
|
// zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten
|
||||||
|
// it only once to the requested chordal-deviation tolerance.
|
||||||
|
const QuinticBezier corner_curve {{
|
||||||
|
{-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.},
|
||||||
|
{0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance}
|
||||||
|
}};
|
||||||
|
std::vector<Vec2d> curve_coefficients;
|
||||||
|
flatten_bezier(corner_curve, deviation, curve_coefficients);
|
||||||
|
|
||||||
|
auto translated_point = [min_x, min_y](size_t idx) {
|
||||||
|
Point p = hilbert_n_to_xy(idx);
|
||||||
|
return Point(p.x() + min_x, p.y() + min_y);
|
||||||
|
};
|
||||||
|
auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); };
|
||||||
|
bool has_last_output = false;
|
||||||
|
Vec2d last_output;
|
||||||
|
// Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates
|
||||||
|
// to avoid emitting zero-length extrusion segments.
|
||||||
|
auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) {
|
||||||
|
if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) {
|
||||||
|
output.add_point(point);
|
||||||
|
last_output = point;
|
||||||
|
has_last_output = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Vec2d previous = to_vec2d(translated_point(0));
|
||||||
|
Vec2d corner = to_vec2d(translated_point(1));
|
||||||
|
add_point(previous);
|
||||||
|
// Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of
|
||||||
|
// its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline.
|
||||||
|
for (size_t i = 1; i + 1 < point_count; ++i) {
|
||||||
|
const Vec2d next = to_vec2d(translated_point(i + 1));
|
||||||
|
const Vec2d incoming = (corner - previous).normalized();
|
||||||
|
const Vec2d outgoing = (next - corner).normalized();
|
||||||
|
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
|
||||||
|
|
||||||
|
if (std::abs(cross) < EPSILON) {
|
||||||
|
add_point(corner);
|
||||||
|
} else {
|
||||||
|
add_point(corner - corner_distance * incoming);
|
||||||
|
for (const Vec2d &coefficient : curve_coefficients)
|
||||||
|
add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = corner;
|
||||||
|
corner = next;
|
||||||
|
}
|
||||||
|
add_point(corner);
|
||||||
|
}
|
||||||
|
|
||||||
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
|
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
|
||||||
{
|
{
|
||||||
if (output.clips())
|
if (output.clips())
|
||||||
@@ -296,6 +437,24 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo
|
|||||||
generate_hilbert_curve(min_x, min_y, max_x, max_y, output);
|
generate_hilbert_curve(min_x, min_y, max_x, max_y, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||||
|
const FillParams ¶ms, InfillPolylineOutput &output)
|
||||||
|
{
|
||||||
|
const double smooth_factor = std::isfinite(params.smooth_factor) ?
|
||||||
|
std::clamp(params.smooth_factor, 0., 1.) : 0.;
|
||||||
|
if (smooth_factor == 0.) {
|
||||||
|
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double corner_distance = 0.5 * smooth_factor;
|
||||||
|
if (output.clips())
|
||||||
|
generate_smooth_hilbert_curve(
|
||||||
|
min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast<InfillPolylineClipper&>(output));
|
||||||
|
else
|
||||||
|
generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output);
|
||||||
|
}
|
||||||
|
|
||||||
template<typename Output>
|
template<typename Output>
|
||||||
static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output)
|
static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ protected:
|
|||||||
friend class InfillPolylineClipper;
|
friend class InfillPolylineClipper;
|
||||||
|
|
||||||
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) = 0;
|
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) = 0;
|
||||||
|
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||||
|
const FillParams & /* params */, InfillPolylineOutput &output)
|
||||||
|
{
|
||||||
|
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class FillArchimedeanChords : public FillPlanePath
|
class FillArchimedeanChords : public FillPlanePath
|
||||||
@@ -75,6 +80,8 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
bool centered() const override { return false; }
|
bool centered() const override { return false; }
|
||||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
|
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
|
||||||
|
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||||
|
const FillParams ¶ms, InfillPolylineOutput &output) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class FillOctagramSpiral : public FillPlanePath
|
class FillOctagramSpiral : public FillPlanePath
|
||||||
|
|||||||
@@ -1037,6 +1037,7 @@ static std::vector<std::string> s_Preset_print_options{
|
|||||||
"fill_multiline",
|
"fill_multiline",
|
||||||
"gyroid_optimized",
|
"gyroid_optimized",
|
||||||
"sparse_infill_pattern",
|
"sparse_infill_pattern",
|
||||||
|
"sparse_infill_smooth_factor",
|
||||||
"lateral_lattice_angle_1",
|
"lateral_lattice_angle_1",
|
||||||
"lateral_lattice_angle_2",
|
"lateral_lattice_angle_2",
|
||||||
"infill_overhang_angle",
|
"infill_overhang_angle",
|
||||||
|
|||||||
@@ -3457,6 +3457,18 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->enum_labels.push_back(L("Octagram Spiral"));
|
def->enum_labels.push_back(L("Octagram Spiral"));
|
||||||
def->set_default_value(new ConfigOptionEnum<InfillPattern>(ipCrossHatch));
|
def->set_default_value(new ConfigOptionEnum<InfillPattern>(ipCrossHatch));
|
||||||
|
|
||||||
|
def = this->add("sparse_infill_smooth_factor", coPercent);
|
||||||
|
def->label = L("Sparse infill smooth factor");
|
||||||
|
def->category = L("Strength");
|
||||||
|
def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, "
|
||||||
|
"while 100% produces the largest possible curves between adjacent infill lines. "
|
||||||
|
"Currently applies only to the Hilbert Curve.");
|
||||||
|
def->sidetext = "%";
|
||||||
|
def->min = 0;
|
||||||
|
def->max = 100;
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->set_default_value(new ConfigOptionPercent(0));
|
||||||
|
|
||||||
def = this->add("top_surface_acceleration", coFloats);
|
def = this->add("top_surface_acceleration", coFloats);
|
||||||
def->label = L("Top surface");
|
def->label = L("Top surface");
|
||||||
def->category = L("Speed");
|
def->category = L("Speed");
|
||||||
|
|||||||
@@ -1264,6 +1264,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionString, sparse_infill_rotate_template))
|
((ConfigOptionString, sparse_infill_rotate_template))
|
||||||
((ConfigOptionPercent, sparse_infill_density))
|
((ConfigOptionPercent, sparse_infill_density))
|
||||||
((ConfigOptionEnum<InfillPattern>, sparse_infill_pattern))
|
((ConfigOptionEnum<InfillPattern>, sparse_infill_pattern))
|
||||||
|
((ConfigOptionPercent, sparse_infill_smooth_factor))
|
||||||
((ConfigOptionFloat, lateral_lattice_angle_1))
|
((ConfigOptionFloat, lateral_lattice_angle_1))
|
||||||
((ConfigOptionFloat, lateral_lattice_angle_2))
|
((ConfigOptionFloat, lateral_lattice_angle_2))
|
||||||
((ConfigOptionFloat, infill_overhang_angle))
|
((ConfigOptionFloat, infill_overhang_angle))
|
||||||
|
|||||||
@@ -1409,6 +1409,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
|||||||
|| opt_key == "infill_overhang_angle") {
|
|| opt_key == "infill_overhang_angle") {
|
||||||
steps.emplace_back(posInfill);
|
steps.emplace_back(posInfill);
|
||||||
} else if (opt_key == "sparse_infill_pattern"
|
} else if (opt_key == "sparse_infill_pattern"
|
||||||
|
|| opt_key == "sparse_infill_smooth_factor"
|
||||||
|| opt_key == "symmetric_infill_y_axis"
|
|| opt_key == "symmetric_infill_y_axis"
|
||||||
|| opt_key == "infill_shift_step"
|
|| opt_key == "infill_shift_step"
|
||||||
|| opt_key == "sparse_infill_rotate_template"
|
|| opt_key == "sparse_infill_rotate_template"
|
||||||
|
|||||||
@@ -707,6 +707,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
|||||||
bool has_top_shell = has_top_shell_layers && config->option<ConfigOptionPercent>("top_surface_density")->value > 0;
|
bool has_top_shell = has_top_shell_layers && config->option<ConfigOptionPercent>("top_surface_density")->value > 0;
|
||||||
bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0;
|
bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0;
|
||||||
bool has_solid_infill = has_top_shell_layers || has_bottom_shell;
|
bool has_solid_infill = has_top_shell_layers || has_bottom_shell;
|
||||||
|
toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve);
|
||||||
toggle_field("top_surface_pattern", has_top_shell);
|
toggle_field("top_surface_pattern", has_top_shell);
|
||||||
toggle_field("bottom_surface_pattern", has_bottom_shell);
|
toggle_field("bottom_surface_pattern", has_bottom_shell);
|
||||||
toggle_field("top_surface_density", has_top_shell_layers);
|
toggle_field("top_surface_density", has_top_shell_layers);
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
|
|||||||
{"sparse_infill_density", "", 1},
|
{"sparse_infill_density", "", 1},
|
||||||
{"fill_multiline", "", 1},
|
{"fill_multiline", "", 1},
|
||||||
{"sparse_infill_pattern", "", 1},
|
{"sparse_infill_pattern", "", 1},
|
||||||
|
{"sparse_infill_smooth_factor", "", 1},
|
||||||
{"lateral_lattice_angle_1", "", 1},
|
{"lateral_lattice_angle_1", "", 1},
|
||||||
{"lateral_lattice_angle_2", "", 1},
|
{"lateral_lattice_angle_2", "", 1},
|
||||||
{"infill_overhang_angle", "", 1},
|
{"infill_overhang_angle", "", 1},
|
||||||
|
|||||||
@@ -2816,6 +2816,7 @@ void TabPrint::build()
|
|||||||
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
|
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
|
||||||
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
|
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
|
||||||
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
|
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
|
||||||
|
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor");
|
||||||
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
|
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
|
||||||
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
|
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
|
||||||
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
|
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_preset_setting_id.cpp
|
test_preset_setting_id.cpp
|
||||||
test_preset_diff.cpp
|
test_preset_diff.cpp
|
||||||
test_elephant_foot_compensation.cpp
|
test_elephant_foot_compensation.cpp
|
||||||
|
test_fill_plane_path.cpp
|
||||||
test_geometry.cpp
|
test_geometry.cpp
|
||||||
test_multimaterial_segmentation.cpp
|
test_multimaterial_segmentation.cpp
|
||||||
test_placeholder_parser.cpp
|
test_placeholder_parser.cpp
|
||||||
|
|||||||
164
tests/libslic3r/test_fill_plane_path.cpp
Normal file
164
tests/libslic3r/test_fill_plane_path.cpp
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "libslic3r/Fill/FillPlanePath.hpp"
|
||||||
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr double output_scale = 1'000'000.;
|
||||||
|
|
||||||
|
class TestableHilbertCurve : public FillHilbertCurve
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7)
|
||||||
|
{
|
||||||
|
InfillPolylineOutput output(output_scale);
|
||||||
|
FillParams params;
|
||||||
|
params.smooth_factor = smooth_factor;
|
||||||
|
FillHilbertCurve::generate(0, 0, max_coordinate, max_coordinate, resolution, params, output);
|
||||||
|
return std::move(output.result());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
double path_length(const Points &points)
|
||||||
|
{
|
||||||
|
double length = 0.;
|
||||||
|
for (size_t i = 1; i < points.size(); ++i)
|
||||||
|
length += (points[i] - points[i - 1]).cast<double>().norm();
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
double discrete_curvature_at(const Points &points, const Point &point)
|
||||||
|
{
|
||||||
|
const auto point_it = std::find(points.begin(), points.end(), point);
|
||||||
|
REQUIRE(point_it != points.end());
|
||||||
|
const size_t point_idx = size_t(std::distance(points.begin(), point_it));
|
||||||
|
REQUIRE(point_idx > 0);
|
||||||
|
REQUIRE(point_idx + 1 < points.size());
|
||||||
|
|
||||||
|
const Vec2d incoming = (points[point_idx] - points[point_idx - 1]).cast<double>() / output_scale;
|
||||||
|
const Vec2d outgoing = (points[point_idx + 1] - points[point_idx]).cast<double>() / output_scale;
|
||||||
|
const Vec2d chord = incoming + outgoing;
|
||||||
|
const double cross = std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x());
|
||||||
|
return 2. * cross / (incoming.norm() * outgoing.norm() * chord.norm());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Hilbert curve exposes a smoothing factor", "[FillPlanePath]")
|
||||||
|
{
|
||||||
|
const ConfigOptionDef *factor_def = print_config_def.get("sparse_infill_smooth_factor");
|
||||||
|
REQUIRE(factor_def != nullptr);
|
||||||
|
REQUIRE(factor_def->type == coPercent);
|
||||||
|
REQUIRE_THAT(factor_def->min, Catch::Matchers::WithinAbs(0., 1e-12));
|
||||||
|
REQUIRE_THAT(factor_def->max, Catch::Matchers::WithinAbs(100., 1e-12));
|
||||||
|
REQUIRE_THAT(factor_def->get_default_value<ConfigOptionPercent>()->value,
|
||||||
|
Catch::Matchers::WithinAbs(0., 1e-12));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Hilbert curve smoothing rounds right angle turns", "[FillPlanePath]")
|
||||||
|
{
|
||||||
|
const Points sharp = TestableHilbertCurve().generate_points(0.005);
|
||||||
|
const Points smooth = TestableHilbertCurve().generate_points(0.005, 1.);
|
||||||
|
|
||||||
|
REQUIRE(smooth.front() == sharp.front());
|
||||||
|
REQUIRE(smooth.back() == sharp.back());
|
||||||
|
REQUIRE(smooth.size() > sharp.size());
|
||||||
|
|
||||||
|
bool has_turn = false;
|
||||||
|
for (size_t i = 1; i < smooth.size(); ++i) {
|
||||||
|
const Vec2d segment = (smooth[i] - smooth[i - 1]).cast<double>();
|
||||||
|
REQUIRE(segment.squaredNorm() > 0.);
|
||||||
|
}
|
||||||
|
for (size_t i = 1; i + 1 < smooth.size(); ++i) {
|
||||||
|
const Vec2d incoming = (smooth[i] - smooth[i - 1]).cast<double>();
|
||||||
|
const Vec2d outgoing = (smooth[i + 1] - smooth[i]).cast<double>();
|
||||||
|
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
|
||||||
|
const double cosine = incoming.dot(outgoing) / (incoming.norm() * outgoing.norm());
|
||||||
|
has_turn |= std::abs(cross) > 0.;
|
||||||
|
REQUIRE(cosine > 0.);
|
||||||
|
}
|
||||||
|
REQUIRE(has_turn);
|
||||||
|
|
||||||
|
const coord_t upper_bound = coord_t(7 * output_scale);
|
||||||
|
for (const Point &point : smooth) {
|
||||||
|
REQUIRE(point.x() >= 0);
|
||||||
|
REQUIRE(point.y() >= 0);
|
||||||
|
REQUIRE(point.x() <= upper_bound);
|
||||||
|
REQUIRE(point.y() <= upper_bound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Smoothed Hilbert curve honors path resolution", "[FillPlanePath]")
|
||||||
|
{
|
||||||
|
const Points coarse = TestableHilbertCurve().generate_points(0.1, 1.);
|
||||||
|
const Points fine = TestableHilbertCurve().generate_points(0.001, 1.);
|
||||||
|
|
||||||
|
REQUIRE(fine.size() > coarse.size());
|
||||||
|
REQUIRE(fine.front() == coarse.front());
|
||||||
|
REQUIRE(fine.back() == coarse.back());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Smoothed Hilbert corners use a uniform subdivision depth", "[FillPlanePath]")
|
||||||
|
{
|
||||||
|
const Points smooth = TestableHilbertCurve().generate_points(0.0035, 1., 1);
|
||||||
|
const Point curve_entry(0, coord_t(0.5 * output_scale));
|
||||||
|
const Point curve_exit(coord_t(0.5 * output_scale), coord_t(output_scale));
|
||||||
|
|
||||||
|
const auto entry_it = std::find(smooth.begin(), smooth.end(), curve_entry);
|
||||||
|
REQUIRE(entry_it != smooth.end());
|
||||||
|
const auto exit_it = std::find(entry_it, smooth.end(), curve_exit);
|
||||||
|
REQUIRE(exit_it != smooth.end());
|
||||||
|
|
||||||
|
const size_t segment_count = size_t(std::distance(entry_it, exit_it));
|
||||||
|
REQUIRE(segment_count > 1);
|
||||||
|
REQUIRE((segment_count & (segment_count - 1)) == 0);
|
||||||
|
|
||||||
|
double previous_length = (entry_it[1] - entry_it[0]).cast<double>().norm();
|
||||||
|
REQUIRE(previous_length > 0.);
|
||||||
|
double max_length_ratio = 1.;
|
||||||
|
for (size_t segment = 1; segment < segment_count; ++segment) {
|
||||||
|
const double current_length = (entry_it[segment + 1] - entry_it[segment]).cast<double>().norm();
|
||||||
|
REQUIRE(current_length > 0.);
|
||||||
|
max_length_ratio = std::max(max_length_ratio,
|
||||||
|
std::max(current_length / previous_length, previous_length / current_length));
|
||||||
|
previous_length = current_length;
|
||||||
|
}
|
||||||
|
REQUIRE(max_length_ratio < 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature", "[FillPlanePath]")
|
||||||
|
{
|
||||||
|
const Points coarse = TestableHilbertCurve().generate_points(0.005, 0.5, 1);
|
||||||
|
const Points fine = TestableHilbertCurve().generate_points(0.0001, 0.5, 1);
|
||||||
|
const Point first_curve_entry(0, coord_t(0.75 * output_scale));
|
||||||
|
|
||||||
|
const double coarse_entry_curvature = discrete_curvature_at(coarse, first_curve_entry);
|
||||||
|
const double fine_entry_curvature = discrete_curvature_at(fine, first_curve_entry);
|
||||||
|
REQUIRE(coarse_entry_curvature > 0.);
|
||||||
|
REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]")
|
||||||
|
{
|
||||||
|
const Points sharp = TestableHilbertCurve().generate_points(0.005);
|
||||||
|
const Points half_smooth = TestableHilbertCurve().generate_points(0.005, 0.5);
|
||||||
|
const Points full_smooth = TestableHilbertCurve().generate_points(0.005, 1.);
|
||||||
|
const Points invalid_factor = TestableHilbertCurve().generate_points(
|
||||||
|
0.005, std::numeric_limits<double>::quiet_NaN());
|
||||||
|
|
||||||
|
REQUIRE(full_smooth.front() == half_smooth.front());
|
||||||
|
REQUIRE(full_smooth.back() == half_smooth.back());
|
||||||
|
REQUIRE(path_length(full_smooth) < path_length(half_smooth));
|
||||||
|
REQUIRE(invalid_factor == sharp);
|
||||||
|
|
||||||
|
for (size_t i = 1; i < full_smooth.size(); ++i)
|
||||||
|
REQUIRE((full_smooth[i] - full_smooth[i - 1]).squaredNorm() > 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user