mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-26 11:27:41 +00:00
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.
This commit is contained in:
@@ -108,6 +108,22 @@ const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
|
|||||||
return m_cached_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)
|
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
|
||||||
{
|
{
|
||||||
m_corner_points.clear();
|
m_corner_points.clear();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -47,36 +48,57 @@ public:
|
|||||||
|
|
||||||
template<typename Emit> void push(const Vec2d &point, Emit &emit)
|
template<typename Emit> 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);
|
emit(point);
|
||||||
m_previous = point;
|
m_window[m_held++] = point;
|
||||||
} else if (m_pending > 1) {
|
return;
|
||||||
round_corner(m_previous, m_corner, point);
|
|
||||||
for (const Vec2d &corner_point : m_corner_points)
|
|
||||||
emit(corner_point);
|
|
||||||
m_previous = m_corner;
|
|
||||||
}
|
}
|
||||||
m_corner = point;
|
if (m_held > 1 && is_on_straight_run(m_window[m_held - 2], m_window[m_held - 1], point)) {
|
||||||
m_pending = std::min(m_pending + 1, 2);
|
// 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.
|
// Emits the last point of the path and prepares the smoother for a new one.
|
||||||
template<typename Emit> void flush(Emit &emit)
|
template<typename Emit> void flush(Emit &emit)
|
||||||
{
|
{
|
||||||
if (m_pending > 1)
|
if (m_held > 2)
|
||||||
emit(m_corner);
|
emit_corner(m_window[0], m_window[1], m_window[2], emit);
|
||||||
m_pending = 0;
|
if (m_held > 1)
|
||||||
|
emit(m_window[m_held - 1]);
|
||||||
|
m_held = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
template<typename Emit> 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.
|
// Fills m_corner_points with the points replacing the corner vertex.
|
||||||
void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next);
|
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
|
// 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.
|
// (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner.
|
||||||
const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing);
|
const std::vector<Vec2d>& 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
|
// Fraction of the shorter adjoining leg consumed on each side of a corner. Half of a leg is the
|
||||||
// is the maximum, otherwise the curves of two adjacent corners would overlap.
|
// maximum, otherwise the curves of two adjacent corners would overlap.
|
||||||
const double m_corner_distance_ratio;
|
const double m_corner_distance_ratio;
|
||||||
const double m_tolerance;
|
const double m_tolerance;
|
||||||
const double m_max_corner_distance;
|
const double m_max_corner_distance;
|
||||||
@@ -88,10 +110,11 @@ private:
|
|||||||
double m_cached_cosine { 0. };
|
double m_cached_cosine { 0. };
|
||||||
bool m_has_cached_coefficients { false };
|
bool m_has_cached_coefficients { false };
|
||||||
|
|
||||||
Vec2d m_previous { Vec2d::Zero() };
|
// The corners seen last, kept free of vertices that merely split a straight leg. The middle one
|
||||||
Vec2d m_corner { Vec2d::Zero() };
|
// is rounded once the third arrives, which is what makes its outgoing leg final.
|
||||||
// Number of points held back: none, the first point of a path, or a corner candidate.
|
std::array<Vec2d, 3> m_window { Vec2d::Zero(), Vec2d::Zero(), Vec2d::Zero() };
|
||||||
int m_pending { 0 };
|
// 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.
|
// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone.
|
||||||
|
|||||||
@@ -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.front() == sharp.front());
|
||||||
REQUIRE(retrace.back() == sharp.back());
|
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.)));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user