From a5223279acc9cf3952296aab7d0728345b482676 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 25 Aug 2026 12:46:34 -0300 Subject: [PATCH] Fix uneven corner rounding in multiline infill (#15352) * Skip straight-run splits in corner smoothing Teach `CornerSmoother` to treat vertices that only continue a straight segment as part of the same leg instead of rounding them as corners. The smoother now keeps a three-point window so it can emit a corner only once both adjoining legs are known, which avoids unnecessary corner processing while preserving real turns such as hairpins. * Add regression test for split-leg smoothing Adds a FillCornerSmoothing regression test covering polylines with an extra collinear vertex in a straight run. The test ensures corner smoothing treats split and unsplit geometry identically, preventing inconsistent rounding radii in triangular/grid infill paths. --- src/libslic3r/Fill/FillCornerSmoothing.cpp | 16 +++++ src/libslic3r/Fill/FillCornerSmoothing.hpp | 59 +++++++++++++------ .../libslic3r/test_fill_corner_smoothing.cpp | 21 +++++++ 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/Fill/FillCornerSmoothing.cpp b/src/libslic3r/Fill/FillCornerSmoothing.cpp index 2af9f6bb9c..dbce39d572 100644 --- a/src/libslic3r/Fill/FillCornerSmoothing.cpp +++ b/src/libslic3r/Fill/FillCornerSmoothing.cpp @@ -108,6 +108,22 @@ const std::vector& CornerSmoother::curve_coefficients( return m_cached_coefficients; } +bool CornerSmoother::is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next) +{ + const Vec2d incoming_leg = vertex - previous; + const Vec2d outgoing_leg = next - vertex; + const double incoming_length = incoming_leg.norm(); + const double outgoing_length = outgoing_leg.norm(); + // A vertex repeating one of its neighbours carries no direction of its own. + if (incoming_length < EPSILON || outgoing_length < EPSILON) + return true; + + const Vec2d incoming = incoming_leg / incoming_length; + const Vec2d outgoing = outgoing_leg / outgoing_length; + return incoming.dot(outgoing) > 0. && + std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x()) < EPSILON; +} + void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next) { m_corner_points.clear(); diff --git a/src/libslic3r/Fill/FillCornerSmoothing.hpp b/src/libslic3r/Fill/FillCornerSmoothing.hpp index 1852fc4c67..7f2ead229a 100644 --- a/src/libslic3r/Fill/FillCornerSmoothing.hpp +++ b/src/libslic3r/Fill/FillCornerSmoothing.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -47,36 +48,57 @@ public: template void push(const Vec2d &point, Emit &emit) { - if (m_pending == 0) { + if (m_held == 0) { + // The first point of a path is an end, not a corner, and stays where it is. emit(point); - m_previous = point; - } else if (m_pending > 1) { - round_corner(m_previous, m_corner, point); - for (const Vec2d &corner_point : m_corner_points) - emit(corner_point); - m_previous = m_corner; + m_window[m_held++] = point; + return; } - m_corner = point; - m_pending = std::min(m_pending + 1, 2); + if (m_held > 1 && is_on_straight_run(m_window[m_held - 2], m_window[m_held - 1], point)) { + // The newest vertex only splits a straight leg, so the leg runs on to this point instead. + m_window[m_held - 1] = point; + return; + } + if (m_held < 3) { + m_window[m_held++] = point; + return; + } + // Both legs of the middle vertex are complete now, so its curve can no longer grow. + emit_corner(m_window[0], m_window[1], m_window[2], emit); + m_window[0] = m_window[1]; + m_window[1] = m_window[2]; + m_window[2] = point; } // Emits the last point of the path and prepares the smoother for a new one. template void flush(Emit &emit) { - if (m_pending > 1) - emit(m_corner); - m_pending = 0; + if (m_held > 2) + emit_corner(m_window[0], m_window[1], m_window[2], emit); + if (m_held > 1) + emit(m_window[m_held - 1]); + m_held = 0; } private: + template void emit_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next, Emit &emit) + { + round_corner(previous, corner, next); + for (const Vec2d &corner_point : m_corner_points) + emit(corner_point); + } + + // Tells a vertex that only continues a straight leg (or repeats its predecessor) from a corner. + // A path doubling back on itself is not one, that vertex is a hairpin and stays where it is. + static bool is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next); // Fills m_corner_points with the points replacing the corner vertex. void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next); // Flattens the canonical corner curve of the given size and turn into coordinates of the // (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner. const std::vector& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing); - // Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment - // is the maximum, otherwise the curves of two adjacent corners would overlap. + // Fraction of the shorter adjoining leg consumed on each side of a corner. Half of a leg is the + // maximum, otherwise the curves of two adjacent corners would overlap. const double m_corner_distance_ratio; const double m_tolerance; const double m_max_corner_distance; @@ -88,10 +110,11 @@ private: double m_cached_cosine { 0. }; bool m_has_cached_coefficients { false }; - Vec2d m_previous { Vec2d::Zero() }; - Vec2d m_corner { Vec2d::Zero() }; - // Number of points held back: none, the first point of a path, or a corner candidate. - int m_pending { 0 }; + // The corners seen last, kept free of vertices that merely split a straight leg. The middle one + // is rounded once the third arrives, which is what makes its outgoing leg final. + std::array m_window { Vec2d::Zero(), Vec2d::Zero(), Vec2d::Zero() }; + // How many of them are filled in. + int m_held { 0 }; }; // Rounds the corners of already scaled paths in place. Paths of less than three points are left alone. diff --git a/tests/libslic3r/test_fill_corner_smoothing.cpp b/tests/libslic3r/test_fill_corner_smoothing.cpp index f2c25e816d..a9e752f250 100644 --- a/tests/libslic3r/test_fill_corner_smoothing.cpp +++ b/tests/libslic3r/test_fill_corner_smoothing.cpp @@ -171,3 +171,24 @@ TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", REQUIRE(retrace.front() == sharp.front()); REQUIRE(retrace.back() == sharp.back()); } + +TEST_CASE("Corner smoothing ignores vertices splitting a straight leg", "[FillCornerSmoothing][Regression]") +{ + // The triangular and grid infills emit a vertex halfway along the straight run joining two of + // their corners. Measuring the legs up to that vertex instead of up to the next corner let the + // rounding reach only half as far there as it did into the very same run elsewhere in the + // pattern, so geometrically identical corners came out rounded to different radii. + const Polyline plain{ Point::new_scale(0., 20.), Point::new_scale(10., 0.), + Point::new_scale(20., 0.), Point::new_scale(30., 20.) }; + Polyline split = plain; + split.points.insert(split.points.begin() + 2, Point::new_scale(15., 0.)); + + Polyline smooth_plain = plain; + smooth_polyline_corners(smooth_plain, 1., tolerance); + Polyline smooth_split = split; + smooth_polyline_corners(smooth_split, 1., tolerance); + + REQUIRE(smooth_split.points == smooth_plain.points); + // Both corners reach the middle of the 10mm run they share, which the extra vertex sat on. + REQUIRE(contains(smooth_plain, Point::new_scale(15., 0.))); +}