mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-25 07:34:03 -06:00

* SPE-2486: Refactor function apply_mm_segmentation() to prepare support for fuzzy skin painting. (cherry picked from commit 2c06c81159f7aadd6ac20c7a7583c8f4959a5601) * SPE-2585: Fix empty layers when multi-material painting and modifiers are used. (cherry picked from commit 4b3da02ec26d43bfad91897cb34779fb21419e3e) * Update project structure to match Prusa * SPE-2486: Add a new gizmo for fuzzy skin painting. (cherry picked from commit 886faac74ebe6978b828f51be62d26176e2900e5) * Fix render * Remove duplicated painting gizmo `render_triangles` code * SPE-2486: Extend multi-material segmentation to allow segmentation of any painted faces. (cherry picked from commit 519f5eea8e3be0d7c2cd5d030323ff264727e3d0) --------- Co-authored-by: Lukáš Hejl <hejl.lukas@gmail.com> * SPE-2486: Implement segmentation of layers based on fuzzy skin painting. (cherry picked from commit 800b742b950438c5ed8323693074b6171300131c) * SPE-2486: Separate fuzzy skin implementation into the separate file. (cherry picked from commit efd95c1c66dc09fca7695fb82405056c687c2291) * Move more fuzzy code to separate file * Don't hide fuzzy skin option, so it can be applied to paint on fuzzy * Fix build * Add option group for fuzzy skin * Update icon color * Fix reset painting * Update UI style * Store fuzzy painting in bbs_3mf * Add missing fuzzy paint code * SPE-2486: Limit the depth of the painted fuzzy skin regions to make regions cover just external perimeters. This reduces the possibility of artifacts that could happen during regions merging. (cherry picked from commit fa2663f02647f80b239da4f45d92ef66f5ce048a) * Update icons --------- Co-authored-by: yw4z <ywsyildiz@gmail.com> * Make the region compatible check a separate function * Only warn about multi-material if it's truly multi-perimeters * Improve gizmo UI & tooltips --------- Co-authored-by: Lukáš Hejl <hejl.lukas@gmail.com> Co-authored-by: yw4z <ywsyildiz@gmail.com>
391 lines
16 KiB
C++
391 lines
16 KiB
C++
#include <random>
|
|
|
|
#include "libslic3r/Algorithm/LineSplit.hpp"
|
|
#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp"
|
|
#include "libslic3r/Arachne/utils/ExtrusionLine.hpp"
|
|
#include "libslic3r/Layer.hpp"
|
|
#include "libslic3r/PerimeterGenerator.hpp"
|
|
#include "libslic3r/Point.hpp"
|
|
#include "libslic3r/Polygon.hpp"
|
|
#include "libslic3r/Print.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
|
|
#include "FuzzySkin.hpp"
|
|
|
|
#include "libnoise/noise.h"
|
|
|
|
// #define DEBUG_FUZZY
|
|
|
|
using namespace Slic3r;
|
|
|
|
namespace Slic3r::Feature::FuzzySkin {
|
|
|
|
// Produces a random value between 0 and 1. Thread-safe.
|
|
static double random_value() {
|
|
thread_local std::random_device rd;
|
|
// Hash thread ID for random number seed if no hardware rng seed is available
|
|
thread_local std::mt19937 gen(rd.entropy() > 0 ? rd() : std::hash<std::thread::id>()(std::this_thread::get_id()));
|
|
thread_local std::uniform_real_distribution<double> dist(0.0, 1.0);
|
|
return dist(gen);
|
|
}
|
|
|
|
class UniformNoise: public noise::module::Module {
|
|
public:
|
|
UniformNoise(): Module (GetSourceModuleCount ()) {};
|
|
|
|
virtual int GetSourceModuleCount() const { return 0; }
|
|
virtual double GetValue(double x, double y, double z) const { return random_value() * 2 - 1; }
|
|
};
|
|
|
|
static std::unique_ptr<noise::module::Module> get_noise_module(const FuzzySkinConfig& cfg) {
|
|
if (cfg.noise_type == NoiseType::Perlin) {
|
|
auto perlin_noise = noise::module::Perlin();
|
|
perlin_noise.SetFrequency(1 / cfg.noise_scale);
|
|
perlin_noise.SetOctaveCount(cfg.noise_octaves);
|
|
perlin_noise.SetPersistence(cfg.noise_persistence);
|
|
return std::make_unique<noise::module::Perlin>(perlin_noise);
|
|
} else if (cfg.noise_type == NoiseType::Billow) {
|
|
auto billow_noise = noise::module::Billow();
|
|
billow_noise.SetFrequency(1 / cfg.noise_scale);
|
|
billow_noise.SetOctaveCount(cfg.noise_octaves);
|
|
billow_noise.SetPersistence(cfg.noise_persistence);
|
|
return std::make_unique<noise::module::Billow>(billow_noise);
|
|
} else if (cfg.noise_type == NoiseType::RidgedMulti) {
|
|
auto ridged_multi_noise = noise::module::RidgedMulti();
|
|
ridged_multi_noise.SetFrequency(1 / cfg.noise_scale);
|
|
ridged_multi_noise.SetOctaveCount(cfg.noise_octaves);
|
|
return std::make_unique<noise::module::RidgedMulti>(ridged_multi_noise);
|
|
} else if (cfg.noise_type == NoiseType::Voronoi) {
|
|
auto voronoi_noise = noise::module::Voronoi();
|
|
voronoi_noise.SetFrequency(1 / cfg.noise_scale);
|
|
voronoi_noise.SetDisplacement(1.0);
|
|
return std::make_unique<noise::module::Voronoi>(voronoi_noise);
|
|
} else {
|
|
return std::make_unique<UniformNoise>();
|
|
}
|
|
}
|
|
|
|
// Thanks Cura developers for this function.
|
|
void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkinConfig& cfg)
|
|
{
|
|
std::unique_ptr<noise::module::Module> noise = get_noise_module(cfg);
|
|
|
|
const double min_dist_between_points = cfg.point_distance * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value
|
|
const double range_random_point_dist = cfg.point_distance / 2.;
|
|
double dist_left_over = random_value() * (min_dist_between_points / 2.); // the distance to be traversed on the line before making the first new point
|
|
Point* p0 = &poly.back();
|
|
Points out;
|
|
out.reserve(poly.size());
|
|
for (Point &p1 : poly)
|
|
{
|
|
if (!closed) {
|
|
// Skip the first point for open path
|
|
closed = true;
|
|
p0 = &p1;
|
|
continue;
|
|
}
|
|
// 'a' is the (next) new point between p0 and p1
|
|
Vec2d p0p1 = (p1 - *p0).cast<double>();
|
|
double p0p1_size = p0p1.norm();
|
|
double p0pa_dist = dist_left_over;
|
|
for (; p0pa_dist < p0p1_size;
|
|
p0pa_dist += min_dist_between_points + random_value() * range_random_point_dist)
|
|
{
|
|
Point pa = *p0 + (p0p1 * (p0pa_dist / p0p1_size)).cast<coord_t>();
|
|
double r = noise->GetValue(unscale_(pa.x()), unscale_(pa.y()), slice_z) * cfg.thickness;
|
|
out.emplace_back(pa + (perp(p0p1).cast<double>().normalized() * r).cast<coord_t>());
|
|
}
|
|
dist_left_over = p0pa_dist - p0p1_size;
|
|
p0 = &p1;
|
|
}
|
|
while (out.size() < 3) {
|
|
size_t point_idx = poly.size() - 2;
|
|
out.emplace_back(poly[point_idx]);
|
|
if (point_idx == 0)
|
|
break;
|
|
-- point_idx;
|
|
}
|
|
if (out.size() >= 3)
|
|
poly = std::move(out);
|
|
}
|
|
|
|
// Thanks Cura developers for this function.
|
|
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg)
|
|
{
|
|
std::unique_ptr<noise::module::Module> noise = get_noise_module(cfg);
|
|
|
|
const double min_dist_between_points = cfg.point_distance * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value
|
|
const double range_random_point_dist = cfg.point_distance / 2.;
|
|
double dist_left_over = random_value() * (min_dist_between_points / 2.); // the distance to be traversed on the line before making the first new point
|
|
|
|
auto* p0 = &ext_lines.front();
|
|
Arachne::ExtrusionJunctions out;
|
|
out.reserve(ext_lines.size());
|
|
for (auto& p1 : ext_lines) {
|
|
if (p0->p == p1.p) { // Connect endpoints.
|
|
out.emplace_back(p1.p, p1.w, p1.perimeter_index);
|
|
continue;
|
|
}
|
|
|
|
// 'a' is the (next) new point between p0 and p1
|
|
Vec2d p0p1 = (p1.p - p0->p).cast<double>();
|
|
double p0p1_size = p0p1.norm();
|
|
double p0pa_dist = dist_left_over;
|
|
for (; p0pa_dist < p0p1_size; p0pa_dist += min_dist_between_points + random_value() * range_random_point_dist) {
|
|
Point pa = p0->p + (p0p1 * (p0pa_dist / p0p1_size)).cast<coord_t>();
|
|
double r = noise->GetValue(unscale_(pa.x()), unscale_(pa.y()), slice_z) * cfg.thickness;
|
|
out.emplace_back(pa + (perp(p0p1).cast<double>().normalized() * r).cast<coord_t>(), p1.w, p1.perimeter_index);
|
|
}
|
|
dist_left_over = p0pa_dist - p0p1_size;
|
|
p0 = &p1;
|
|
}
|
|
|
|
while (out.size() < 3) {
|
|
size_t point_idx = ext_lines.size() - 2;
|
|
out.emplace_back(ext_lines[point_idx].p, ext_lines[point_idx].w, ext_lines[point_idx].perimeter_index);
|
|
if (point_idx == 0)
|
|
break;
|
|
--point_idx;
|
|
}
|
|
|
|
if (ext_lines.back().p == ext_lines.front().p) // Connect endpoints.
|
|
out.front().p = out.back().p;
|
|
|
|
if (out.size() >= 3)
|
|
ext_lines = std::move(out);
|
|
}
|
|
|
|
void group_region_by_fuzzify(PerimeterGenerator& g)
|
|
{
|
|
g.regions_by_fuzzify.clear();
|
|
g.has_fuzzy_skin = false;
|
|
g.has_fuzzy_hole = false;
|
|
|
|
std::unordered_map<FuzzySkinConfig, SurfacesPtr> regions;
|
|
for (auto region : *g.compatible_regions) {
|
|
const auto& region_config = region->region().config();
|
|
const FuzzySkinConfig cfg{region_config.fuzzy_skin,
|
|
scaled<coord_t>(region_config.fuzzy_skin_thickness.value),
|
|
scaled<coord_t>(region_config.fuzzy_skin_point_distance.value),
|
|
region_config.fuzzy_skin_first_layer,
|
|
region_config.fuzzy_skin_noise_type,
|
|
region_config.fuzzy_skin_scale,
|
|
region_config.fuzzy_skin_octaves,
|
|
region_config.fuzzy_skin_persistence};
|
|
auto& surfaces = regions[cfg];
|
|
for (const auto& surface : region->slices.surfaces) {
|
|
surfaces.push_back(&surface);
|
|
}
|
|
|
|
if (cfg.type != FuzzySkinType::None) {
|
|
g.has_fuzzy_skin = true;
|
|
if (cfg.type != FuzzySkinType::External) {
|
|
g.has_fuzzy_hole = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (regions.size() == 1) { // optimization
|
|
g.regions_by_fuzzify[regions.begin()->first] = {};
|
|
return;
|
|
}
|
|
|
|
for (auto& it : regions) {
|
|
g.regions_by_fuzzify[it.first] = offset_ex(it.second, ClipperSafetyOffset);
|
|
}
|
|
}
|
|
|
|
bool should_fuzzify(const FuzzySkinConfig& config, const int layer_id, const size_t loop_idx, const bool is_contour)
|
|
{
|
|
const auto fuzziy_type = config.type;
|
|
|
|
if (fuzziy_type == FuzzySkinType::None) {
|
|
return false;
|
|
}
|
|
if (!config.fuzzy_first_layer && layer_id <= 0) {
|
|
// Do not fuzzy first layer unless told to
|
|
return false;
|
|
}
|
|
|
|
const bool fuzzify_contours = loop_idx == 0 || fuzziy_type == FuzzySkinType::AllWalls;
|
|
const bool fuzzify_holes = fuzzify_contours && (fuzziy_type == FuzzySkinType::All || fuzziy_type == FuzzySkinType::AllWalls);
|
|
|
|
return is_contour ? fuzzify_contours : fuzzify_holes;
|
|
}
|
|
|
|
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, const size_t loop_idx, const bool is_contour)
|
|
{
|
|
Polygon fuzzified;
|
|
|
|
const auto slice_z = perimeter_generator.slice_z;
|
|
const auto& regions = perimeter_generator.regions_by_fuzzify;
|
|
if (regions.size() == 1) { // optimization
|
|
const auto& config = regions.begin()->first;
|
|
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour);
|
|
if (!fuzzify) {
|
|
return polygon;
|
|
}
|
|
|
|
fuzzified = polygon;
|
|
fuzzy_polyline(fuzzified.points, true, slice_z, config);
|
|
return fuzzified;
|
|
}
|
|
|
|
// Find all affective regions
|
|
std::vector<std::pair<const FuzzySkinConfig&, const ExPolygons&>> fuzzified_regions;
|
|
fuzzified_regions.reserve(regions.size());
|
|
for (const auto& region : regions) {
|
|
if (should_fuzzify(region.first, perimeter_generator.layer_id, loop_idx, is_contour)) {
|
|
fuzzified_regions.emplace_back(region.first, region.second);
|
|
}
|
|
}
|
|
if (fuzzified_regions.empty()) {
|
|
return polygon;
|
|
}
|
|
|
|
#ifdef DEBUG_FUZZY
|
|
{
|
|
int i = 0;
|
|
for (const auto& r : fuzzified_regions) {
|
|
BoundingBox bbox = get_extents(perimeter_generator.slices->surfaces);
|
|
bbox.offset(scale_(1.));
|
|
::Slic3r::SVG svg(debug_out_path("fuzzy_traverse_loops_%d_%d_%d_region_%d.svg", perimeter_generator.layer_id,
|
|
loop.is_contour ? 0 : 1, loop.depth, i)
|
|
.c_str(),
|
|
bbox);
|
|
svg.draw_outline(perimeter_generator.slices->surfaces);
|
|
svg.draw_outline(loop.polygon, "green");
|
|
svg.draw(r.second, "red", 0.5);
|
|
svg.draw_outline(r.second, "red");
|
|
svg.Close();
|
|
i++;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Split the loops into lines with different config, and fuzzy them separately
|
|
fuzzified = polygon;
|
|
for (const auto& r : fuzzified_regions) {
|
|
const auto splitted = Algorithm::split_line(fuzzified, r.second, true);
|
|
if (splitted.empty()) {
|
|
// No intersection, skip
|
|
continue;
|
|
}
|
|
|
|
// Fuzzy splitted polygon
|
|
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
|
|
// The entire polygon is fuzzified
|
|
fuzzy_polyline(fuzzified.points, true, slice_z, r.first);
|
|
} else {
|
|
Points segment;
|
|
segment.reserve(splitted.size());
|
|
fuzzified.points.clear();
|
|
|
|
const auto fuzzy_current_segment = [&segment, &fuzzified, &r, slice_z]() {
|
|
fuzzified.points.push_back(segment.front());
|
|
const auto back = segment.back();
|
|
fuzzy_polyline(segment, false, slice_z, r.first);
|
|
fuzzified.points.insert(fuzzified.points.end(), segment.begin(), segment.end());
|
|
fuzzified.points.push_back(back);
|
|
segment.clear();
|
|
};
|
|
|
|
for (const auto& p : splitted) {
|
|
if (p.clipped) {
|
|
segment.push_back(p.p);
|
|
} else {
|
|
if (segment.empty()) {
|
|
fuzzified.points.push_back(p.p);
|
|
} else {
|
|
segment.push_back(p.p);
|
|
fuzzy_current_segment();
|
|
}
|
|
}
|
|
}
|
|
if (!segment.empty()) {
|
|
// Close the loop
|
|
segment.push_back(splitted.front().p);
|
|
fuzzy_current_segment();
|
|
}
|
|
}
|
|
}
|
|
|
|
return fuzzified;
|
|
}
|
|
|
|
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour)
|
|
{
|
|
const auto slice_z = perimeter_generator.slice_z;
|
|
const auto& regions = perimeter_generator.regions_by_fuzzify;
|
|
if (regions.size() == 1) { // optimization
|
|
const auto& config = regions.begin()->first;
|
|
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
|
|
if (fuzzify)
|
|
fuzzy_extrusion_line(extrusion->junctions, slice_z, config);
|
|
} else {
|
|
// Find all affective regions
|
|
std::vector<std::pair<const FuzzySkinConfig&, const ExPolygons&>> fuzzified_regions;
|
|
fuzzified_regions.reserve(regions.size());
|
|
for (const auto& region : regions) {
|
|
if (should_fuzzify(region.first, perimeter_generator.layer_id, extrusion->inset_idx, is_contour)) {
|
|
fuzzified_regions.emplace_back(region.first, region.second);
|
|
}
|
|
}
|
|
if (!fuzzified_regions.empty()) {
|
|
// Split the loops into lines with different config, and fuzzy them separately
|
|
for (const auto& r : fuzzified_regions) {
|
|
const auto splitted = Algorithm::split_line(*extrusion, r.second, false);
|
|
if (splitted.empty()) {
|
|
// No intersection, skip
|
|
continue;
|
|
}
|
|
|
|
// Fuzzy splitted extrusion
|
|
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
|
|
// The entire polygon is fuzzified
|
|
fuzzy_extrusion_line(extrusion->junctions, slice_z, r.first);
|
|
} else {
|
|
const auto current_ext = extrusion->junctions;
|
|
std::vector<Arachne::ExtrusionJunction> segment;
|
|
segment.reserve(current_ext.size());
|
|
extrusion->junctions.clear();
|
|
|
|
const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z]() {
|
|
extrusion->junctions.push_back(segment.front());
|
|
const auto back = segment.back();
|
|
fuzzy_extrusion_line(segment, slice_z, r.first);
|
|
extrusion->junctions.insert(extrusion->junctions.end(), segment.begin(), segment.end());
|
|
extrusion->junctions.push_back(back);
|
|
segment.clear();
|
|
};
|
|
|
|
const auto to_ex_junction = [¤t_ext](const Algorithm::SplitLineJunction& j) -> Arachne::ExtrusionJunction {
|
|
Arachne::ExtrusionJunction res = current_ext[j.get_src_index()];
|
|
if (!j.is_src()) {
|
|
res.p = j.p;
|
|
}
|
|
return res;
|
|
};
|
|
|
|
for (const auto& p : splitted) {
|
|
if (p.clipped) {
|
|
segment.push_back(to_ex_junction(p));
|
|
} else {
|
|
if (segment.empty()) {
|
|
extrusion->junctions.push_back(to_ex_junction(p));
|
|
} else {
|
|
segment.push_back(to_ex_junction(p));
|
|
fuzzy_current_segment();
|
|
}
|
|
}
|
|
}
|
|
if (!segment.empty()) {
|
|
fuzzy_current_segment();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace Slic3r::Feature::FuzzySkin
|