Files
OrcaSlicer/src/libslic3r/Fill/FillCornerSmoothing.cpp
2026-08-18 12:19:27 -03:00

227 lines
10 KiB
C++

#include <array>
#include "FillCornerSmoothing.hpp"
namespace Slic3r {
// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly
// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin.
static constexpr const double min_smoothed_turn_cosine = -0.9;
// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not
// orthonormal for turns other than a right angle.
using QuinticBezier = std::array<Vec2d, 6>;
static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, 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.
auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; };
const Vec2d chord = in_plane(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 = in_plane(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 Vec2d &incoming, const Vec2d &outgoing, 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, incoming, outgoing, 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 it can be shared with the straight leg feeding into it.
output.clear();
output.reserve(subcurves.size());
for (const QuinticBezier &c : subcurves)
output.emplace_back(c.back());
}
const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing)
{
const double cosine = incoming.dot(outgoing);
// Corners of the same size and turn angle are congruent, so they flatten identically. An infill
// path walks over the very same corner over and over again, the Hilbert curve over a single one.
if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine)
return m_cached_coefficients;
// One canonical corner running from -corner_distance along the incoming leg to corner_distance
// along the outgoing one. 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.
const double d = corner_distance;
const QuinticBezier corner_curve {{
{-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d}
}};
// Retain a finite positive tolerance if the smoother was set up with an invalid one.
const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON;
flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients);
m_cached_distance = corner_distance;
m_cached_cosine = cosine;
m_has_cached_coefficients = true;
return m_cached_coefficients;
}
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
{
m_corner_points.clear();
const Vec2d incoming_leg = corner - previous;
const Vec2d outgoing_leg = next - corner;
const double incoming_length = incoming_leg.norm();
const double outgoing_length = outgoing_leg.norm();
if (incoming_length < EPSILON || outgoing_length < EPSILON) {
m_corner_points.emplace_back(corner);
return;
}
const Vec2d incoming = incoming_leg / incoming_length;
const Vec2d outgoing = outgoing_leg / outgoing_length;
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
// A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above.
if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) {
m_corner_points.emplace_back(corner);
return;
}
// Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart.
double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length);
if (m_max_corner_distance > 0.)
corner_distance = std::min(corner_distance, m_max_corner_distance);
const Vec2d curve_start = corner - corner_distance * incoming;
const Vec2d curve_end = corner + corner_distance * outgoing;
if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) {
m_corner_points.emplace_back(corner);
return;
}
const std::vector<Vec2d> &coefficients = curve_coefficients(corner_distance, incoming, outgoing);
m_corner_points.reserve(coefficients.size() + 1);
m_corner_points.emplace_back(curve_start);
for (const Vec2d &coefficient : coefficients)
m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
}
// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices
// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a
// path returning to where it started retraces its way back and is not a loop.
static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother)
{
// A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes
// feeding the smoother the last vertex first, whose own output point is then dropped again.
size_t skip = polygon ? 1 : 0;
Points smoothed;
smoothed.reserve(2 * points.size());
auto emit = [&smoothed, &skip](const Vec2d &point) {
if (skip > 0) {
--skip;
return;
}
smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5)));
};
if (polygon)
smoother.push(points.back().cast<double>(), emit);
for (const Point &point : points)
smoother.push(point.cast<double>(), emit);
if (polygon)
// Wrap the first vertex around, so that the last one is a corner as well.
smoother.push(points.front().cast<double>(), emit);
smoother.flush(emit);
if (polygon)
// The flushed point is the wrapped first vertex, which a polygon does not store.
smoothed.pop_back();
return smoothed;
}
void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance,
const double max_corner_distance, const CornerFilter &corner_filter)
{
CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter);
if (!smoother.enabled() || polyline.size() < 3)
return;
polyline.points = smooth_corners(polyline.points, false, smoother);
// Rounding back to the integer grid may collapse neighbouring samples of a curve.
polyline.remove_duplicate_points();
}
void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance,
const double max_corner_distance, const CornerFilter &corner_filter)
{
if (sanitize_smooth_factor(smooth_factor) == 0.)
return;
for (Polyline &polyline : polylines)
smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter);
}
void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance,
const double max_corner_distance, const CornerFilter &corner_filter)
{
CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter);
if (!smoother.enabled())
return;
for (Polygon &polygon : polygons) {
if (polygon.size() < 3)
continue;
polygon.points = smooth_corners(polygon.points, true, smoother);
polygon.remove_duplicate_points();
// The curves of the first and of the last corner may have met on the segment they share. A
// polygon closes implicitly, so it must not repeat its first vertex at the end.
if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back())
polygon.points.pop_back();
}
}
} // namespace Slic3r