mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-25 19:07:47 +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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
@@ -47,36 +48,57 @@ public:
|
||||
|
||||
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);
|
||||
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<typename Emit> 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<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.
|
||||
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<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
|
||||
// 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<Vec2d, 3> 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.
|
||||
|
||||
@@ -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.)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user