Improve fuzzy skin with modifier (#6759)

* Pass all compatible regions to perimeter generator

* Simplify & unify fuzzify detection

* Simplify `to_thick_polyline`

* Group regions by fuzzy skin settings

* Initial code structure of multi-regional fuzzy skin

* Determine fuzzy type by all compatible regions

* Add fuzzy region debug

* Implement the line split algorithm

* Do splitted fuzzy in classic mode

* Disable debug macros

* Fix infinit loop issue when segment points are out of order

* Fix path connection

* Implement splitted fuzzy in Arachne mode
This commit is contained in:
Noisyfox 2024-09-23 00:41:17 +08:00 committed by GitHub
parent 3d3633f110
commit 9d3d242453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 707 additions and 92 deletions

View file

@ -0,0 +1,316 @@
#include "LineSplit.hpp"
#include "AABBTreeLines.hpp"
#include "SVG.hpp"
#include "Utils.hpp"
//#define DEBUG_SPLIT_LINE
namespace Slic3r {
namespace Algorithm {
#ifdef DEBUG_SPLIT_LINE
static std::atomic<std::uint32_t> g_dbg_id = 0;
#endif
// Z for points from clip polygon
static constexpr auto CLIP_IDX = std::numeric_limits<ClipperLib_Z::cInt>::max();
static void cb_split_line(const ClipperZUtils::ZPoint& e1bot,
const ClipperZUtils::ZPoint& e1top,
const ClipperZUtils::ZPoint& e2bot,
const ClipperZUtils::ZPoint& e2top,
ClipperZUtils::ZPoint& pt)
{
coord_t zs[4]{e1bot.z(), e1top.z(), e2bot.z(), e2top.z()};
std::sort(zs, zs + 4);
pt.z() = -(zs[0] + 1);
}
static bool is_src(const ClipperZUtils::ZPoint& p) { return p.z() >= 0 && p.z() != CLIP_IDX; }
static bool is_clip(const ClipperZUtils::ZPoint& p) { return p.z() == CLIP_IDX; }
static bool is_new(const ClipperZUtils::ZPoint& p) { return p.z() < 0; }
static size_t to_src_idx(const ClipperZUtils::ZPoint& p)
{
assert(!is_clip(p));
if (is_src(p)) {
return p.z();
} else {
return -p.z() - 1;
}
}
static Point to_point(const ClipperZUtils::ZPoint& p) { return {p.x(), p.y()}; }
using SplitNode = std::vector<ClipperZUtils::ZPath*>;
// Note: p cannot be one of the line end
static bool point_on_line(const Point& p, const Line& l)
{
// Check collinear
const auto d1 = l.b - l.a;
const auto d2 = p - l.a;
if (d1.x() * d2.y() != d1.y() * d2.x()) {
return false;
}
// Make sure p is in between line.a and line.b
if (l.a.x() != l.b.x())
return (p.x() > l.a.x()) == (p.x() < l.b.x());
else
return (p.y() > l.a.y()) == (p.y() < l.b.y());
}
SplittedLine do_split_line(const ClipperZUtils::ZPath& path, const ExPolygons& clip, bool closed)
{
assert(path.size() > 1);
#ifdef DEBUG_SPLIT_LINE
const auto dbg_path_points = ClipperZUtils::from_zpath<false>(path);
BoundingBox dbg_bbox = get_extents(clip);
dbg_bbox.merge(get_extents(dbg_path_points));
dbg_bbox.offset(scale_(1.));
const std::uint32_t dbg_id = g_dbg_id++;
{
::Slic3r::SVG svg(debug_out_path("do_split_line_%d_input.svg", dbg_id).c_str(), dbg_bbox);
svg.draw(clip, "red", 0.5);
svg.draw_outline(clip, "red");
svg.draw(Polyline{dbg_path_points});
svg.draw(dbg_path_points);
svg.Close();
}
#endif
ClipperZUtils::ZPaths intersections;
// Perform an intersection
{
// Convert clip polygon to closed contours
ClipperZUtils::ZPaths clip_path;
for (const auto& exp : clip) {
clip_path.emplace_back(ClipperZUtils::to_zpath<false>(exp.contour.points, CLIP_IDX));
for (const Polygon& hole : exp.holes)
clip_path.emplace_back(ClipperZUtils::to_zpath<false>(hole.points, CLIP_IDX));
}
ClipperLib_Z::Clipper zclipper;
zclipper.PreserveCollinear(true);
zclipper.ZFillFunction(cb_split_line);
zclipper.AddPaths(clip_path, ClipperLib_Z::ptClip, true);
zclipper.AddPath(path, ClipperLib_Z::ptSubject, false);
ClipperLib_Z::PolyTree polytree;
zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(std::move(polytree), intersections);
}
if (intersections.empty()) {
return {};
}
#ifdef DEBUG_SPLIT_LINE
{
int i = 0;
for (const auto& segment : intersections) {
::Slic3r::SVG svg(debug_out_path("do_split_line_%d_seg_%d.svg", dbg_id, i).c_str(), dbg_bbox);
svg.draw(clip, "red", 0.5);
svg.draw_outline(clip, "red");
const auto segment_points = ClipperZUtils::from_zpath<false>(segment);
svg.draw(Polyline{segment_points});
for (const ClipperZUtils::ZPoint& p : segment) {
const auto z = p.z();
if (is_new(p)) {
svg.draw(to_point(p), "yellow");
} else if (is_clip(p)) {
svg.draw(to_point(p), "red");
} else {
svg.draw(to_point(p), "black");
}
}
svg.Close();
i++;
}
}
#endif
// Connect the intersection back to the remaining loop
std::vector<SplitNode> split_chain;
{
// AABBTree over source paths.
// Only built if necessary, that is if any of the clipped segment has first point came from clip polygon,
// and we need to find out which source edge that point came from.
AABBTreeLines::LinesDistancer<Line> aabb_tree;
const auto resolve_clip_point = [&path, &aabb_tree](ClipperZUtils::ZPoint& zp) {
if (!is_clip(zp)) {
return;
}
if (aabb_tree.get_lines().empty()) {
Lines lines;
lines.reserve(path.size() - 1);
for (auto it = path.begin() + 1; it != path.end(); ++it) {
lines.emplace_back(to_point(it[-1]), to_point(*it));
}
aabb_tree = AABBTreeLines::LinesDistancer(lines);
}
const Point p = to_point(zp);
const auto possible_edges = aabb_tree.all_lines_in_radius(p, SCALED_EPSILON);
assert(!possible_edges.empty());
for (const size_t l : possible_edges) {
// Check if the point is on the line
const Line line(to_point(path[l]), to_point(path[l + 1]));
if (p == line.a) {
zp.z() = path[l].z();
break;
}
if (p == line.b) {
zp.z() = path[l + 1].z();
break;
}
if (point_on_line(p, line)) {
zp.z() = -(path[l].z() + 1);
break;
}
}
if (is_clip(zp)) {
// Too bad! Couldn't find the src edge, so we just pick the first one and hope it works
zp.z() = -(path[possible_edges[0]].z() + 1);
}
};
split_chain.assign(path.size(), {});
for (ClipperZUtils::ZPath& segment : intersections) {
assert(segment.size() >= 2);
// Resolve all clip points
std::for_each(segment.begin(), segment.end(), resolve_clip_point);
// Ensure the point order in segment
std::sort(segment.begin(), segment.end(), [&path](const ClipperZUtils::ZPoint& a, const ClipperZUtils::ZPoint& b) -> bool {
if (is_new(a) && is_new(b) && a.z() == b.z()) {
// Make sure a point is closer to the src point than b
const auto src = to_point(path[-a.z() - 1]);
return (to_point(a) - src).squaredNorm() < (to_point(b) - src).squaredNorm();
}
const auto a_idx = to_src_idx(a);
const auto b_idx = to_src_idx(b);
if (a_idx == b_idx) {
// On same line, prefer the src point first
return is_src(a);
} else {
return a_idx < b_idx;
}
});
// Chain segment back to the original path
ClipperZUtils::ZPoint& front = segment.front();
const ClipperZUtils::ZPoint* previous_src_point;
if (is_src(front)) {
// The segment starts with a point from src path, which means apart from the last point,
// all other points on this segment should come from the src path or the clip polygon
// Connect the segment to the src path
auto& node = split_chain[front.z()];
node.insert(node.begin(), &segment);
previous_src_point = &front;
} else if (is_new(front)) {
const auto id = -front.z() - 1; // Get the src path index
const ClipperZUtils::ZPoint& src_p = path[id]; // Get the corresponding src point
const auto dist2 = (front - src_p).block<2, 1>(0,0).squaredNorm(); // Distance between the src point and current point
// Find the place on the src line that current point should lie on
auto& node = split_chain[id];
auto it = std::find_if(node.begin(), node.end(), [dist2, &src_p](const ClipperZUtils::ZPath* p) {
const ClipperZUtils::ZPoint& p_front = p->front();
if (is_src(p_front)) {
return false;
}
const auto dist2_2 = (p_front - src_p).block<2, 1>(0, 0).squaredNorm();
return dist2_2 > dist2;
});
// Insert this split
node.insert(it, &segment);
previous_src_point = &src_p;
} else {
assert(false);
}
// Once we figured out the start point, we can then normalize the remaining points on the segment
for (ClipperZUtils::ZPoint& p : segment) {
assert(!is_new(p) || p == front || p == segment.back()); // Only the first and last point can be a new intersection
if (is_src(p)) {
previous_src_point = &p;
} else if (is_clip(p)) {
// Treat point from clip polygon as new point
p.z() = -(previous_src_point->z() + 1);
}
}
}
}
// Now we reconstruct the final path by connecting splits
SplittedLine result;
size_t idx = 0;
while (idx < split_chain.size()) {
const ClipperZUtils::ZPoint& p = path[idx];
const auto& node = split_chain[idx];
if (node.empty()) {
result.emplace_back(to_point(p), false, idx);
idx++;
} else {
if (!is_src(node.front()->front())) {
const auto& last = result.back();
if (result.empty() || last.get_src_index() != to_src_idx(p)) {
result.emplace_back(to_point(p), false, idx);
}
}
for (const auto segment : node) {
for (const ClipperZUtils::ZPoint& sp : *segment) {
assert(!is_clip(sp.z()));
result.emplace_back(to_point(sp), true, sp.z());
}
result.back().clipped = false; // Mark the end of the clipped line
}
// Determine the next start point
const auto back = result.back().src_idx;
if (back < 0) {
auto next_idx = -back - 1;
if (next_idx == idx) {
next_idx++;
} else if (split_chain[next_idx].empty()) {
next_idx++;
}
idx = next_idx;
} else {
result.pop_back();
idx = back;
}
}
}
#ifdef DEBUG_SPLIT_LINE
{
::Slic3r::SVG svg(debug_out_path("do_split_line_%d_result.svg", dbg_id).c_str(), dbg_bbox);
svg.draw(clip, "red", 0.5);
svg.draw_outline(clip, "red");
for (auto it = result.begin() + 1; it != result.end(); ++it) {
const auto& a = *(it - 1);
const auto& b = *it;
const bool clipped = a.clipped;
const Line l(a.p, b.p);
svg.draw(l, clipped ? "yellow" : "black");
}
svg.Close();
}
#endif
if (closed) {
// Remove last point which was duplicated
result.pop_back();
}
return result;
}
} // Algorithm
} // Slic3r

View file

@ -0,0 +1,71 @@
#ifndef SRC_LIBSLIC3R_ALGORITHM_LINE_SPLIT_HPP_
#define SRC_LIBSLIC3R_ALGORITHM_LINE_SPLIT_HPP_
#include "clipper2/clipper.core.h"
#include "ClipperZUtils.hpp"
namespace Slic3r {
namespace Algorithm {
struct SplitLineJunction
{
Point p;
// true if the line between this point and the next point is inside the clip polygon (or on the edge of the clip polygon)
bool clipped;
// Index from the original input.
// - If this junction is presented in the source polygon/polyline, this is the index of the point with in the source;
// - if this point in a new point that caused by the intersection, this will be -(1+index of the first point of the source line involved in this intersection);
// - if this junction came from the clip polygon, it will be treated as new point.
int64_t src_idx;
SplitLineJunction(const Point& p, bool clipped, int64_t src_idx)
: p(p)
, clipped(clipped)
, src_idx(src_idx) {}
bool is_src() const { return src_idx >= 0; }
size_t get_src_index() const
{
if (is_src()) {
return src_idx;
} else {
return -src_idx - 1;
}
}
};
using SplittedLine = std::vector<SplitLineJunction>;
SplittedLine do_split_line(const ClipperZUtils::ZPath& path, const ExPolygons& clip, bool closed);
// Return the splitted line, or empty if no intersection found
template<class PathType>
SplittedLine split_line(const PathType& path, const ExPolygons& clip, bool closed)
{
if (path.empty()) {
return {};
}
// Convert the input path into an open ZPath
ClipperZUtils::ZPath p;
p.reserve(path.size() + closed ? 1 : 0);
ClipperLib_Z::cInt z = 0;
for (const auto& point : path) {
p.emplace_back(point.x(), point.y(), z);
z++;
}
if (closed) {
// duplicate the first point at the end to make a closed path open
p.emplace_back(p.front());
p.back().z() = z;
}
return do_split_line(p, clip, closed);
}
} // Algorithm
} // Slic3r
#endif /* SRC_LIBSLIC3R_ALGORITHM_LINE_SPLIT_HPP_ */