From fb36d5e73b8fa662ccf6b918581bda414bb53187 Mon Sep 17 00:00:00 2001 From: Valerii Bokhan <80919135+valerii-bokhan@users.noreply.github.com> Date: Sat, 1 Aug 2026 22:58:04 +0200 Subject: [PATCH] Feature: Smooth Factor for the Hilbert Curve sparse infill (#14969) --- src/libslic3r/Fill/Fill.cpp | 12 ++ src/libslic3r/Fill/FillBase.hpp | 3 + src/libslic3r/Fill/FillPlanePath.cpp | 163 +++++++++++++++++++++- src/libslic3r/Fill/FillPlanePath.hpp | 7 + src/libslic3r/Preset.cpp | 1 + src/libslic3r/PrintConfig.cpp | 12 ++ src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 1 + src/slic3r/GUI/ConfigManipulation.cpp | 1 + src/slic3r/GUI/GUI_Factories.cpp | 1 + src/slic3r/GUI/Tab.cpp | 1 + tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_fill_plane_path.cpp | 164 +++++++++++++++++++++++ 13 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 tests/libslic3r/test_fill_plane_path.cpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index f29d4ef3fd..88d87ddb26 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -278,6 +278,9 @@ struct SurfaceFillParams // For Gyroid: when true, use the parameterized "optimized" wave. 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}; bool separated_infills{false}; @@ -316,6 +319,7 @@ struct SurfaceFillParams RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); RETURN_COMPARE_NON_EQUAL(gyroid_optimized); + RETURN_COMPARE_NON_EQUAL(smooth_factor); RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern); RETURN_COMPARE_NON_EQUAL(separated_infills); 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->separated_infills == rhs.separated_infills && this->gyroid_optimized == rhs.gyroid_optimized && + this->smooth_factor == rhs.smooth_factor && this->fill_order == rhs.fill_order; } }; @@ -964,6 +969,11 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value, region_config.sparse_infill_rotate_template.value); 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 { 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.; @@ -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.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.gyroid_optimized = surface_fill.params.gyroid_optimized; + params.smooth_factor = surface_fill.params.smooth_factor; // BBS 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.multiline = surface_fill.params.multiline; params.gyroid_optimized = surface_fill.params.gyroid_optimized; + params.smooth_factor = surface_fill.params.smooth_factor; for (ExPolygon &expoly : surface_fill.expolygons) { // Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon. diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index 8128b9c9d1..d50c29b332 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -82,6 +82,9 @@ struct FillParams // For Gyroid: when true, use the parameterized "optimized" variant. bool gyroid_optimized { false }; + // Orca: corner smoothing factor in the range [0, 1]. + double smooth_factor { 0. }; + // For Lateral lattice coordf_t lateral_lattice_angle_1 { 0.f }; coordf_t lateral_lattice_angle_2 { 0.f }; diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp index 7ce8e4abb3..7c4f285ac6 100644 --- a/src/libslic3r/Fill/FillPlanePath.cpp +++ b/src/libslic3r/Fill/FillPlanePath.cpp @@ -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. snug_bounding_box.translate(-shift.x(), -shift.y()); 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()); } else { // Filling in a snug bounding box, no need to clip. 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()); } } @@ -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; + +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 &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 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 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 +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 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) { 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); } +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(output)); + else + generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output); +} + template static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output) { diff --git a/src/libslic3r/Fill/FillPlanePath.hpp b/src/libslic3r/Fill/FillPlanePath.hpp index 1371e8506a..b4b25b73ae 100644 --- a/src/libslic3r/Fill/FillPlanePath.hpp +++ b/src/libslic3r/Fill/FillPlanePath.hpp @@ -53,6 +53,11 @@ protected: 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, + const FillParams & /* params */, InfillPolylineOutput &output) + { + this->generate(min_x, min_y, max_x, max_y, resolution, output); + } }; class FillArchimedeanChords : public FillPlanePath @@ -75,6 +80,8 @@ public: protected: 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, + const FillParams ¶ms, InfillPolylineOutput &output) override; }; class FillOctagramSpiral : public FillPlanePath diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 775c31b563..3bd277d44a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1037,6 +1037,7 @@ static std::vector s_Preset_print_options{ "fill_multiline", "gyroid_optimized", "sparse_infill_pattern", + "sparse_infill_smooth_factor", "lateral_lattice_angle_1", "lateral_lattice_angle_2", "infill_overhang_angle", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 17dd195df9..f7222b864f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3457,6 +3457,18 @@ void PrintConfigDef::init_fff_params() def->enum_labels.push_back(L("Octagram Spiral")); def->set_default_value(new ConfigOptionEnum(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->label = L("Top surface"); def->category = L("Speed"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 67841c6404..1082c43491 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1264,6 +1264,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionString, sparse_infill_rotate_template)) ((ConfigOptionPercent, sparse_infill_density)) ((ConfigOptionEnum, sparse_infill_pattern)) + ((ConfigOptionPercent, sparse_infill_smooth_factor)) ((ConfigOptionFloat, lateral_lattice_angle_1)) ((ConfigOptionFloat, lateral_lattice_angle_2)) ((ConfigOptionFloat, infill_overhang_angle)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0ef46fac92..9356f937ab 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1409,6 +1409,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_overhang_angle") { steps.emplace_back(posInfill); } else if (opt_key == "sparse_infill_pattern" + || opt_key == "sparse_infill_smooth_factor" || opt_key == "symmetric_infill_y_axis" || opt_key == "infill_shift_step" || opt_key == "sparse_infill_rotate_template" diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index c2643b5d0f..53a3575a57 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -707,6 +707,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool has_top_shell = has_top_shell_layers && config->option("top_surface_density")->value > 0; bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0; 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("bottom_surface_pattern", has_bottom_shell); toggle_field("top_surface_density", has_top_shell_layers); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index a4a60ac4bd..5254492e5d 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -123,6 +123,7 @@ std::map> SettingsFactory::PART_CATE {"sparse_infill_density", "", 1}, {"fill_multiline", "", 1}, {"sparse_infill_pattern", "", 1}, + {"sparse_infill_smooth_factor", "", 1}, {"lateral_lattice_angle_1", "", 1}, {"lateral_lattice_angle_2", "", 1}, {"infill_overhang_angle", "", 1}, diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ef830b7f35..01458b368a 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2816,6 +2816,7 @@ void TabPrint::build() 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("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("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage"); optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag"); diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 7524c27479..dbc6c99f15 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests test_preset_setting_id.cpp test_preset_diff.cpp test_elephant_foot_compensation.cpp + test_fill_plane_path.cpp test_geometry.cpp test_multimaterial_segmentation.cpp test_placeholder_parser.cpp diff --git a/tests/libslic3r/test_fill_plane_path.cpp b/tests/libslic3r/test_fill_plane_path.cpp new file mode 100644 index 0000000000..bbb75dce58 --- /dev/null +++ b/tests/libslic3r/test_fill_plane_path.cpp @@ -0,0 +1,164 @@ +#include + +#include +#include +#include +#include + +#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().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() / output_scale; + const Vec2d outgoing = (points[point_idx + 1] - points[point_idx]).cast() / 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()->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(); + REQUIRE(segment.squaredNorm() > 0.); + } + for (size_t i = 1; i + 1 < smooth.size(); ++i) { + const Vec2d incoming = (smooth[i] - smooth[i - 1]).cast(); + const Vec2d outgoing = (smooth[i + 1] - smooth[i]).cast(); + 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().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().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::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); +}