From bf81236bc537e2f728dc6f7f82c9295583fa3dab Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Mon, 25 May 2026 00:59:56 +0100 Subject: [PATCH] Add contoning top surface coloring method --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Color.hpp | 1 + src/libslic3r/ExtrusionEntity.hpp | 28 +- src/libslic3r/Fill/Fill.cpp | 959 ++++++++++++++++++++-- src/libslic3r/Fill/FillBase.cpp | 43 +- src/libslic3r/Fill/FillBase.hpp | 3 + src/libslic3r/Layer.hpp | 6 +- src/libslic3r/LayerRegion.cpp | 374 ++++++++- src/libslic3r/PerimeterGenerator.cpp | 5 + src/libslic3r/PrintObject.cpp | 18 +- src/libslic3r/TextureMapping.cpp | 48 ++ src/libslic3r/TextureMapping.hpp | 39 +- src/libslic3r/TextureMappingContoning.cpp | 269 ++++++ src/libslic3r/TextureMappingContoning.hpp | 67 ++ src/libslic3r/TextureMappingOffset.cpp | 112 ++- src/libslic3r/TextureMappingOffset.hpp | 9 +- src/slic3r/GUI/Plater.cpp | 168 +++- 17 files changed, 2007 insertions(+), 144 deletions(-) create mode 100644 src/libslic3r/TextureMappingContoning.cpp create mode 100644 src/libslic3r/TextureMappingContoning.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 50b0695f880..c2b1681848d 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -480,6 +480,8 @@ set(lisbslic3r_sources TriangleSetSampling.hpp TextureMappingOffset.cpp TextureMappingOffset.hpp + TextureMappingContoning.cpp + TextureMappingContoning.hpp TextureMapping.cpp TextureMapping.hpp TriangulateWall.cpp diff --git a/src/libslic3r/Color.hpp b/src/libslic3r/Color.hpp index cd1e82d794d..c534a9bd116 100644 --- a/src/libslic3r/Color.hpp +++ b/src/libslic3r/Color.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Slic3r { using RGB = std::array; diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index edd7839cc1e..3701f629815 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -170,7 +170,7 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { this->inset_idx = rhs.inset_idx; } ExtrusionPath(ExtrusionPath &&rhs) : polyline(std::move(rhs.polyline)) , mm3_per_mm(rhs.mm3_per_mm) @@ -179,7 +179,7 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { this->inset_idx = rhs.inset_idx; } ExtrusionPath(const Polyline &polyline, const ExtrusionPath &rhs) : polyline(polyline) , mm3_per_mm(rhs.mm3_per_mm) @@ -188,7 +188,7 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { this->inset_idx = rhs.inset_idx; } ExtrusionPath(Polyline &&polyline, const ExtrusionPath &rhs) : polyline(std::move(polyline)) , mm3_per_mm(rhs.mm3_per_mm) @@ -197,12 +197,13 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { this->inset_idx = rhs.inset_idx; } ExtrusionPath& operator=(const ExtrusionPath& rhs) { m_can_reverse = rhs.m_can_reverse; m_role = rhs.m_role; m_no_extrusion = rhs.m_no_extrusion; + inset_idx = rhs.inset_idx; this->mm3_per_mm = rhs.mm3_per_mm; this->width = rhs.width; this->height = rhs.height; @@ -213,6 +214,7 @@ public: m_can_reverse = rhs.m_can_reverse; m_role = rhs.m_role; m_no_extrusion = rhs.m_no_extrusion; + inset_idx = rhs.inset_idx; this->mm3_per_mm = rhs.mm3_per_mm; this->width = rhs.width; this->height = rhs.height; @@ -329,21 +331,23 @@ public: ExtrusionPaths paths; ExtrusionMultiPath() {} - ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) {} - ExtrusionMultiPath(ExtrusionMultiPath &&rhs) : paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) {} - ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths) {} - ExtrusionMultiPath(const ExtrusionPath &path) {this->paths.push_back(path); m_can_reverse = path.can_reverse(); } + ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) { this->inset_idx = rhs.inset_idx; } + ExtrusionMultiPath(ExtrusionMultiPath &&rhs) : paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) { this->inset_idx = rhs.inset_idx; } + ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths) { if (!this->paths.empty()) this->inset_idx = this->paths.front().inset_idx; } + ExtrusionMultiPath(const ExtrusionPath &path) {this->paths.push_back(path); m_can_reverse = path.can_reverse(); this->inset_idx = path.inset_idx; } ExtrusionMultiPath &operator=(const ExtrusionMultiPath &rhs) { this->paths = rhs.paths; m_can_reverse = rhs.m_can_reverse; + inset_idx = rhs.inset_idx; return *this; } ExtrusionMultiPath &operator=(ExtrusionMultiPath &&rhs) { this->paths = std::move(rhs.paths); m_can_reverse = rhs.m_can_reverse; + inset_idx = rhs.inset_idx; return *this; } @@ -394,12 +398,12 @@ public: ExtrusionPaths paths; ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {} - ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {} - ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role) {} + ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) { if (!this->paths.empty()) this->inset_idx = this->paths.front().inset_idx; } + ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role) { if (!this->paths.empty()) this->inset_idx = this->paths.front().inset_idx; } ExtrusionLoop(const ExtrusionPath &path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role) - { this->paths.push_back(path); } + { this->paths.push_back(path); this->inset_idx = path.inset_idx; } ExtrusionLoop(const ExtrusionPath &&path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role) - { this->paths.emplace_back(std::move(path)); } + { this->paths.emplace_back(std::move(path)); this->inset_idx = this->paths.back().inset_idx; } bool is_loop() const override{ return true; } bool can_reverse() const override { return false; } ExtrusionEntity* clone() const override{ return new ExtrusionLoop (*this); } diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 1915cba6183..37a4799ec80 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include "../ClipperUtils.hpp" @@ -11,6 +13,7 @@ #include "../PrintConfig.hpp" #include "../Surface.hpp" #include "../TextureMapping.hpp" +#include "../TextureMappingContoning.hpp" #include "../TextureMappingOffset.hpp" #include "AABBTreeLines.hpp" @@ -25,11 +28,21 @@ #include "libslic3r.h" #include +#include #include #include +#include namespace Slic3r { +using ThrowIfCanceled = std::function; + +static void check_canceled(const ThrowIfCanceled *throw_if_canceled) +{ + if (throw_if_canceled != nullptr) + (*throw_if_canceled)(); +} + // Calculate infill rotation angle (in radians) for a given layer from a rotation template. // Grammar subset handled (rotation only): // [±]α[*Z or !][joint][-][N|B|T][length][* or !] @@ -287,6 +300,7 @@ struct SurfaceFillParams float texture_mapping_top_surface_min_width_mm = TextureMappingZone::DefaultTopSurfaceImageMinLineWidthMm; float texture_mapping_top_surface_max_width_mm = TextureMappingZone::DefaultTopSurfaceImageMaxLineWidthMm; bool texture_mapping_top_surface_same_layer_partition = false; + bool texture_mapping_top_surface_contoning = false; int texture_mapping_top_surface_component_index = 0; int texture_mapping_top_surface_component_count = 0; @@ -330,6 +344,7 @@ struct SurfaceFillParams RETURN_COMPARE_NON_EQUAL(texture_mapping_top_surface_min_width_mm); RETURN_COMPARE_NON_EQUAL(texture_mapping_top_surface_max_width_mm); RETURN_COMPARE_NON_EQUAL(texture_mapping_top_surface_same_layer_partition); + RETURN_COMPARE_NON_EQUAL(texture_mapping_top_surface_contoning); RETURN_COMPARE_NON_EQUAL(texture_mapping_top_surface_component_index); RETURN_COMPARE_NON_EQUAL(texture_mapping_top_surface_component_count); @@ -368,6 +383,7 @@ struct SurfaceFillParams this->texture_mapping_top_surface_min_width_mm == rhs.texture_mapping_top_surface_min_width_mm && this->texture_mapping_top_surface_max_width_mm == rhs.texture_mapping_top_surface_max_width_mm && this->texture_mapping_top_surface_same_layer_partition == rhs.texture_mapping_top_surface_same_layer_partition && + this->texture_mapping_top_surface_contoning == rhs.texture_mapping_top_surface_contoning && this->texture_mapping_top_surface_component_index == rhs.texture_mapping_top_surface_component_index && this->texture_mapping_top_surface_component_count == rhs.texture_mapping_top_surface_component_count; } @@ -391,6 +407,8 @@ struct TopSurfaceImageStackSlice { size_t component_index = 0; size_t component_count = 0; bool same_layer_partition = false; + bool contoning = false; + bool lower_surface = false; float angle_rad = float(PI / 4.0); ExPolygons area; }; @@ -404,7 +422,17 @@ struct TopSurfaceImageRegionPlan { float max_width_mm = TextureMappingZone::DefaultTopSurfaceImageMaxLineWidthMm; bool fixed_coloring = true; bool same_layer_partition = false; + bool contoning = false; + bool color_lower_surfaces = true; int colored_top_layers = TextureMappingZone::DefaultTopSurfaceImageColoredTopLayers; + int contoning_stack_layers = TextureMappingZone::DefaultTopSurfaceContoningStackLayers; + float contoning_min_feature_mm = 2.f; + float contoning_external_width_mm = 0.4f; +}; + +enum class TopSurfaceImageSourceSurface { + Top, + Bottom }; static float top_surface_image_filament_luminance(const PrintConfig &config, unsigned int component_id) @@ -472,20 +500,674 @@ static std::optional> top_surface_image_equal_blend_backgro return std::array { mixed[0], mixed[1], mixed[2], 1.f }; } -static ExPolygons top_surface_image_visible_top_mask(const Layer &layer, unsigned int zone_id) +static ExPolygons top_surface_image_visible_surface_mask(const Layer &layer, + unsigned int zone_id, + TopSurfaceImageSourceSurface source_surface) { ExPolygons mask; for (const LayerRegion *layerm : layer.regions()) { if (layerm == nullptr || unsigned(std::max(0, layerm->region().config().solid_infill_filament.value)) != zone_id) continue; for (const Surface &surface : layerm->fill_surfaces.surfaces) - if (surface.is_top()) + if ((source_surface == TopSurfaceImageSourceSurface::Top && surface.is_top()) || + (source_surface == TopSurfaceImageSourceSurface::Bottom && surface.surface_type == stBottom)) mask.emplace_back(surface.expolygon); } return mask.size() > 1 ? union_ex(mask) : mask; } -static std::vector top_surface_image_region_plans(const Layer &layer) +static ExPolygons top_surface_image_visible_top_mask(const Layer &layer, unsigned int zone_id) +{ + return top_surface_image_visible_surface_mask(layer, zone_id, TopSurfaceImageSourceSurface::Top); +} + +static ExPolygons top_surface_image_colorable_shell_mask(const Layer &layer, + unsigned int zone_id, + TopSurfaceImageSourceSurface source_surface) +{ + ExPolygons mask; + for (const LayerRegion *layerm : layer.regions()) { + if (layerm == nullptr || unsigned(std::max(0, layerm->region().config().solid_infill_filament.value)) != zone_id) + continue; + for (const Surface &surface : layerm->fill_surfaces.surfaces) + if (surface.surface_type == stInternalSolid || + (source_surface == TopSurfaceImageSourceSurface::Top && surface.is_top()) || + (source_surface == TopSurfaceImageSourceSurface::Bottom && surface.surface_type == stBottom)) + mask.emplace_back(surface.expolygon); + } + return mask.size() > 1 ? union_ex(mask) : mask; +} + +static ExPolygon top_surface_image_cell_expolygon(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y) +{ + Polygon polygon; + polygon.points.reserve(4); + polygon.points.emplace_back(min_x, min_y); + polygon.points.emplace_back(max_x, min_y); + polygon.points.emplace_back(max_x, max_y); + polygon.points.emplace_back(min_x, max_y); + polygon.make_counter_clockwise(); + return ExPolygon(std::move(polygon)); +} + +static bool top_surface_image_contoning_sample_eligible(const TextureMappingOffsetContext &context, + float x_mm, + float y_mm, + float threshold_deg, + TopSurfaceImageSourceSurface source_surface) +{ + const std::optional normal_z = + sample_weight_field_normal_z(context.weight_field, x_mm, y_mm, context.high_resolution_texture_sampling); + const float oriented_normal_z = + normal_z && source_surface == TopSurfaceImageSourceSurface::Bottom ? -*normal_z : (normal_z ? *normal_z : 1.f); + return !normal_z || texture_mapping_contoning_normal_eligible(oriented_normal_z, threshold_deg); +} + +static std::vector top_surface_image_contoning_stack_areas(const Layer &source_layer, + unsigned int zone_id, + int stack_layers, + TopSurfaceImageSourceSurface source_surface, + const ThrowIfCanceled *throw_if_canceled) +{ + std::vector out; + out.reserve(size_t(std::max(0, stack_layers))); + const Layer *stack_layer = &source_layer; + for (int depth = 0; depth < stack_layers && stack_layer != nullptr; ++depth) { + check_canceled(throw_if_canceled); + ExPolygons area = depth == 0 ? + top_surface_image_visible_surface_mask(*stack_layer, zone_id, source_surface) : + top_surface_image_colorable_shell_mask(*stack_layer, zone_id, source_surface); + out.emplace_back(std::move(area)); + stack_layer = source_surface == TopSurfaceImageSourceSurface::Top ? + stack_layer->lower_layer : + stack_layer->upper_layer; + } + return out; +} + +static double top_surface_image_scaled_area_mm2(double scaled_area) +{ + return std::abs(scaled_area) * SCALING_FACTOR * SCALING_FACTOR; +} + +static bool top_surface_image_expolygons_contain_point(const ExPolygons &expolygons, const Point &point) +{ + for (const ExPolygon &expolygon : expolygons) + if (expolygon.contains(point, true)) + return true; + return false; +} + +static int top_surface_image_contoning_local_stack_layers_at_point(const Point &point, + const std::vector &stack_areas, + int max_stack_layers) +{ + int layers = 0; + for (const ExPolygons &stack_area : stack_areas) { + if (layers >= max_stack_layers) + break; + if (stack_area.empty() || !top_surface_image_expolygons_contain_point(stack_area, point)) + break; + ++layers; + } + return layers; +} + +static ExPolygons top_surface_image_contoning_clean_area(ExPolygons &&component_area, + const ExPolygons &clip_area, + const ExPolygons &blocked_area, + float min_feature_mm, + const ThrowIfCanceled *throw_if_canceled) +{ + if (component_area.empty()) + return {}; + check_canceled(throw_if_canceled); + ExPolygons out = union_ex(component_area); + const float closing_radius = float(scale_(std::clamp(min_feature_mm * 0.18f, 0.05f, 0.45f))); + if (closing_radius > float(SCALED_EPSILON)) + out = closing_ex(out, closing_radius, ClipperLib::jtRound); + if (out.empty()) + return {}; + check_canceled(throw_if_canceled); + out = intersection_ex(out, clip_area, ApplySafetyOffset::Yes); + if (!blocked_area.empty()) + out = diff_ex(out, blocked_area, ApplySafetyOffset::Yes); + if (out.empty()) + return {}; + check_canceled(throw_if_canceled); + + ExPolygons simplified; + const double tolerance = scale_(std::clamp(min_feature_mm * 0.12f, 0.03f, 0.25f)); + for (const ExPolygon &expolygon : out) { + check_canceled(throw_if_canceled); + append(simplified, expolygon.simplify(tolerance)); + } + if (simplified.empty()) + return {}; + + ExPolygons filtered; + const double min_area_mm2 = std::max(0.05, double(min_feature_mm) * double(min_feature_mm) * 0.08); + for (ExPolygon &expolygon : simplified) + if (top_surface_image_scaled_area_mm2(expolygon.area()) >= min_area_mm2) + filtered.emplace_back(std::move(expolygon)); + return filtered.empty() ? ExPolygons() : union_ex(filtered); +} + +struct TopSurfaceImageContoningVectorLabel { + std::vector bottom_to_top; + std::array rgb { { 0.f, 0.f, 0.f } }; + std::array oklab { { 0.f, 0.f, 0.f } }; +}; + +struct TopSurfaceImageContoningVectorRegion { + std::vector bottom_to_top; + ExPolygons area; + int cell_count { 0 }; +}; + +static float top_surface_image_contoning_oklab_error(const std::array &lhs, + const std::array &rhs) +{ + const float dl = lhs[0] - rhs[0]; + const float da = lhs[1] - rhs[1]; + const float db = lhs[2] - rhs[2]; + return dl * dl + 4.f * da * da + 4.f * db * db; +} + +static std::optional> top_surface_image_contoning_stack_rgb( + const std::vector &bottom_to_top, + const PrintConfig &print_config) +{ + if (bottom_to_top.empty()) + return std::nullopt; + std::vector> colors; + std::vector weights; + colors.reserve(bottom_to_top.size()); + weights.reserve(bottom_to_top.size()); + const float weight = 1.f / float(bottom_to_top.size()); + for (unsigned int component_id : bottom_to_top) { + if (component_id == 0 || component_id > print_config.filament_colour.values.size()) + return std::nullopt; + ColorRGB color; + if (!decode_color(print_config.filament_colour.get_at(size_t(component_id - 1)), color)) + return std::nullopt; + colors.push_back({ color.r(), color.g(), color.b() }); + weights.emplace_back(weight); + } + return mix_color_solver_components(colors, weights, ColorSolverMixModel::PigmentPainter); +} + +static std::optional> top_surface_image_contoning_component_rgb( + unsigned int component_id, + const PrintConfig &print_config) +{ + if (component_id == 0 || component_id > print_config.filament_colour.values.size()) + return std::nullopt; + ColorRGB color; + if (!decode_color(print_config.filament_colour.get_at(size_t(component_id - 1)), color)) + return std::nullopt; + return std::array{ color.r(), color.g(), color.b() }; +} + +static void top_surface_image_contoning_sort_stack_for_top_color(std::vector &bottom_to_top, + const std::array &mix_rgb, + const PrintConfig &print_config) +{ + if (bottom_to_top.size() < 2) + return; + const std::array mix_oklab = color_solver_oklab_from_srgb(mix_rgb); + std::stable_sort(bottom_to_top.begin(), bottom_to_top.end(), [&mix_oklab, &print_config](unsigned int lhs, unsigned int rhs) { + const std::optional> lhs_rgb = + top_surface_image_contoning_component_rgb(lhs, print_config); + const std::optional> rhs_rgb = + top_surface_image_contoning_component_rgb(rhs, print_config); + const float lhs_error = lhs_rgb ? + top_surface_image_contoning_oklab_error(color_solver_oklab_from_srgb(*lhs_rgb), mix_oklab) : + std::numeric_limits::max(); + const float rhs_error = rhs_rgb ? + top_surface_image_contoning_oklab_error(color_solver_oklab_from_srgb(*rhs_rgb), mix_oklab) : + std::numeric_limits::max(); + return lhs_error > rhs_error; + }); +} + +static float top_surface_image_contoning_sample_pitch_mm(const TopSurfaceImageRegionPlan &plan, + const BoundingBox &bbox) +{ + float pitch = std::clamp(plan.contoning_external_width_mm, + 0.25f, + std::max(0.25f, plan.contoning_min_feature_mm * 0.5f)); + const double width_mm = unscale(bbox.max.x() - bbox.min.x()); + const double height_mm = unscale(bbox.max.y() - bbox.min.y()); + const double max_samples = 350000.0; + if (width_mm > 0.0 && height_mm > 0.0) { + const double estimated = std::ceil(width_mm / double(pitch)) * std::ceil(height_mm / double(pitch)); + if (estimated > max_samples) + pitch = float(std::sqrt(width_mm * height_mm / max_samples)); + } + return std::clamp(pitch, 0.25f, std::max(0.25f, plan.contoning_min_feature_mm)); +} + +static bool top_surface_image_contoning_grid_component_printable(int cell_count, + int min_col, + int max_col, + int min_row, + int max_row, + float pitch_mm, + float min_feature_mm, + float line_width_mm) +{ + if (cell_count <= 0) + return false; + const double area_mm2 = double(cell_count) * double(pitch_mm) * double(pitch_mm); + const double width_mm = double(max_col - min_col + 1) * double(pitch_mm); + const double height_mm = double(max_row - min_row + 1) * double(pitch_mm); + const double min_area_mm2 = + std::max(double(line_width_mm) * double(min_feature_mm), + double(min_feature_mm) * double(min_feature_mm) * 0.20); + const double min_width_mm = std::max(double(line_width_mm), double(pitch_mm)); + if (area_mm2 < min_area_mm2) + return false; + if (width_mm < double(min_feature_mm) && height_mm < double(min_feature_mm)) + return false; + if (std::min(width_mm, height_mm) < min_width_mm && + std::max(width_mm, height_mm) < 2.0 * double(min_feature_mm)) + return false; + return true; +} + +static void top_surface_image_contoning_merge_small_grid_regions( + std::vector &grid, + int cols, + int rows, + const std::vector &labels, + float pitch_mm, + float min_feature_mm, + float line_width_mm, + const ThrowIfCanceled *throw_if_canceled) +{ + if (grid.empty() || cols <= 0 || rows <= 0 || labels.empty()) + return; + + for (int pass = 0; pass < 8; ++pass) { + check_canceled(throw_if_canceled); + std::vector visited(grid.size(), 0); + bool changed = false; + for (int row = 0; row < rows; ++row) { + if ((row & 15) == 0) + check_canceled(throw_if_canceled); + for (int col = 0; col < cols; ++col) { + const int start_idx = row * cols + col; + const int source_label = grid[size_t(start_idx)]; + if (source_label < 0 || visited[size_t(start_idx)]) + continue; + + std::vector queue; + std::vector cells; + std::map neighbor_counts; + queue.push_back(start_idx); + visited[size_t(start_idx)] = 1; + int min_col = col; + int max_col = col; + int min_row = row; + int max_row = row; + for (size_t queue_idx = 0; queue_idx < queue.size(); ++queue_idx) { + if ((queue_idx & 255) == 0) + check_canceled(throw_if_canceled); + const int idx = queue[queue_idx]; + cells.push_back(idx); + const int r = idx / cols; + const int c = idx - r * cols; + min_col = std::min(min_col, c); + max_col = std::max(max_col, c); + min_row = std::min(min_row, r); + max_row = std::max(max_row, r); + const std::array, 4> neighbors{ + std::pair{ c - 1, r }, + std::pair{ c + 1, r }, + std::pair{ c, r - 1 }, + std::pair{ c, r + 1 } + }; + for (const std::pair &neighbor : neighbors) { + const int nc = neighbor.first; + const int nr = neighbor.second; + if (nc < 0 || nc >= cols || nr < 0 || nr >= rows) + continue; + const int nidx = nr * cols + nc; + const int nlabel = grid[size_t(nidx)]; + if (nlabel == source_label) { + if (!visited[size_t(nidx)]) { + visited[size_t(nidx)] = 1; + queue.push_back(nidx); + } + } else if (nlabel >= 0) { + ++neighbor_counts[nlabel]; + } + } + } + + if (top_surface_image_contoning_grid_component_printable(int(cells.size()), + min_col, + max_col, + min_row, + max_row, + pitch_mm, + min_feature_mm, + line_width_mm)) + continue; + + int best_label = -1; + float best_error = std::numeric_limits::max(); + int best_contact = -1; + for (const auto &entry : neighbor_counts) { + const int neighbor_label = entry.first; + if (neighbor_label < 0 || + neighbor_label >= int(labels.size()) || + source_label >= int(labels.size())) + continue; + const float error = + top_surface_image_contoning_oklab_error(labels[size_t(source_label)].oklab, + labels[size_t(neighbor_label)].oklab); + if (error < best_error - 1e-6f || + (std::abs(error - best_error) <= 1e-6f && entry.second > best_contact)) { + best_label = neighbor_label; + best_error = error; + best_contact = entry.second; + } + } + if (best_label < 0) + continue; + check_canceled(throw_if_canceled); + for (int idx : cells) + grid[size_t(idx)] = best_label; + changed = true; + } + } + if (!changed) + break; + } +} + +static ExPolygons top_surface_image_contoning_area_from_grid_label(const std::vector &grid, + int cols, + int rows, + int label, + coord_t min_x, + coord_t min_y, + coord_t step, + const BoundingBox &bbox, + const ExPolygons &clip_area, + const ExPolygons &blocked_area, + float min_feature_mm, + const ThrowIfCanceled *throw_if_canceled) +{ + ExPolygons cells; + for (int row = 0; row < rows; ++row) { + if ((row & 15) == 0) + check_canceled(throw_if_canceled); + for (int col = 0; col < cols; ++col) { + const int idx = row * cols + col; + if (grid[size_t(idx)] != label) + continue; + const coord_t x0 = min_x + coord_t(col) * step; + const coord_t y0 = min_y + coord_t(row) * step; + const coord_t x1 = std::min(x0 + step, bbox.max.x()); + const coord_t y1 = std::min(y0 + step, bbox.max.y()); + if (x1 <= x0 || y1 <= y0) + continue; + cells.emplace_back(top_surface_image_cell_expolygon(x0, y0, x1, y1)); + } + } + return top_surface_image_contoning_clean_area(std::move(cells), clip_area, blocked_area, min_feature_mm, throw_if_canceled); +} + +static std::vector top_surface_image_contoning_vector_regions( + const TopSurfaceImageRegionPlan &plan, + const Layer &source_layer, + const ExPolygons &area, + const PrintObject &object, + const TextureMappingZone &zone, + const PrintConfig &print_config, + int depth, + const TextureMappingContoningSolver &solver, + TopSurfaceImageSourceSurface source_surface, + const ThrowIfCanceled *throw_if_canceled) +{ + std::vector regions; + if (area.empty() || !solver.valid()) + return regions; + check_canceled(throw_if_canceled); + + std::optional sample_z_mm; + if (source_surface == TopSurfaceImageSourceSurface::Bottom) + sample_z_mm = float(source_layer.bottom_z()); + std::optional context = + build_texture_mapping_offset_context_for_layer(object, + source_layer, + zone, + plan.zone_id, + solver.component_ids().front(), + plan.max_width_mm, + float(source_layer.height), + std::nullopt, + plan.min_width_mm, + top_surface_image_equal_blend_background(print_config, solver.component_ids()), + sample_z_mm); + if (!context) + return regions; + + const BoundingBox bbox = get_extents(area); + if (!bbox.defined) + return regions; + + const float pitch_mm = top_surface_image_contoning_sample_pitch_mm(plan, bbox); + const coord_t step = std::max(1, scale_(double(pitch_mm))); + const coord_t min_x = (bbox.min.x() / step) * step; + const coord_t min_y = (bbox.min.y() / step) * step; + const int cols = std::max(0, int(std::ceil(double(bbox.max.x() - min_x) / double(step)))); + const int rows = std::max(0, int(std::ceil(double(bbox.max.y() - min_y) / double(step)))); + if (cols <= 0 || rows <= 0) + return regions; + + std::vector grid(size_t(cols) * size_t(rows), -1); + std::vector labels; + std::map, int> label_by_stack; + const float threshold_deg = + std::clamp(zone.top_surface_contoning_angle_threshold_deg, + TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg, + TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg); + const int stack_layers = std::clamp(plan.contoning_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + const std::vector source_stack_areas = + top_surface_image_contoning_stack_areas(source_layer, plan.zone_id, stack_layers, source_surface, throw_if_canceled); + + for (int row = 0; row < rows; ++row) { + if ((row & 15) == 0) + check_canceled(throw_if_canceled); + for (int col = 0; col < cols; ++col) { + const coord_t x0 = min_x + coord_t(col) * step; + const coord_t y0 = min_y + coord_t(row) * step; + const coord_t x1 = std::min(x0 + step, bbox.max.x()); + const coord_t y1 = std::min(y0 + step, bbox.max.y()); + if (x1 <= x0 || y1 <= y0) + continue; + const Point sample_point((x0 + x1) / 2, (y0 + y1) / 2); + if (!top_surface_image_expolygons_contain_point(area, sample_point)) + continue; + const int local_stack_layers = + top_surface_image_contoning_local_stack_layers_at_point(sample_point, source_stack_areas, stack_layers); + if (depth >= local_stack_layers) + continue; + const int solve_layers = std::min({ local_stack_layers, stack_layers, 4 }); + if (solve_layers <= 0) + continue; + const float sample_x_mm = unscale(sample_point.x()); + const float sample_y_mm = unscale(sample_point.y()); + if (!top_surface_image_contoning_sample_eligible(*context, sample_x_mm, sample_y_mm, threshold_deg, source_surface)) + continue; + const std::optional> rgb = + sample_weight_field_rgb(context->weight_field, + sample_x_mm, + sample_y_mm, + context->high_resolution_texture_sampling); + if (!rgb) + continue; + TextureMappingContoningStack stack = solver.solve(*rgb, solve_layers); + if (stack.bottom_to_top.empty()) + continue; + std::optional> stack_rgb = + top_surface_image_contoning_stack_rgb(stack.bottom_to_top, print_config); + if (!stack_rgb) + continue; + top_surface_image_contoning_sort_stack_for_top_color(stack.bottom_to_top, *stack_rgb, print_config); + auto label_it = label_by_stack.find(stack.bottom_to_top); + int label = -1; + if (label_it == label_by_stack.end()) { + TopSurfaceImageContoningVectorLabel label_data; + label_data.bottom_to_top = stack.bottom_to_top; + label_data.rgb = *stack_rgb; + label_data.oklab = color_solver_oklab_from_srgb(*stack_rgb); + label = int(labels.size()); + labels.emplace_back(std::move(label_data)); + label_by_stack.emplace(labels.back().bottom_to_top, label); + } else { + label = label_it->second; + } + grid[size_t(row * cols + col)] = label; + } + } + + if (labels.empty()) + return regions; + + top_surface_image_contoning_merge_small_grid_regions(grid, + cols, + rows, + labels, + pitch_mm, + plan.contoning_min_feature_mm, + plan.contoning_external_width_mm, + throw_if_canceled); + check_canceled(throw_if_canceled); + + std::vector cell_counts(labels.size(), 0); + for (int label : grid) + if (label >= 0 && label < int(cell_counts.size())) + ++cell_counts[size_t(label)]; + + std::vector label_order; + for (size_t idx = 0; idx < cell_counts.size(); ++idx) + if (cell_counts[idx] > 0) + label_order.emplace_back(int(idx)); + std::sort(label_order.begin(), label_order.end(), [&cell_counts](int lhs, int rhs) { + return cell_counts[size_t(lhs)] > cell_counts[size_t(rhs)]; + }); + + ExPolygons taken; + for (int label : label_order) { + check_canceled(throw_if_canceled); + ExPolygons label_area = + top_surface_image_contoning_area_from_grid_label(grid, + cols, + rows, + label, + min_x, + min_y, + step, + bbox, + area, + taken, + plan.contoning_min_feature_mm, + throw_if_canceled); + if (label_area.empty()) + continue; + append(taken, label_area); + TopSurfaceImageContoningVectorRegion region; + region.bottom_to_top = labels[size_t(label)].bottom_to_top; + region.cell_count = cell_counts[size_t(label)]; + region.area = std::move(label_area); + regions.emplace_back(std::move(region)); + } + + if (!regions.empty()) { + check_canceled(throw_if_canceled); + ExPolygons covered = union_ex(taken); + ExPolygons leftover = diff_ex(area, covered, ApplySafetyOffset::Yes); + if (!leftover.empty()) { + append(regions.front().area, std::move(leftover)); + regions.front().area = union_ex(regions.front().area); + } + } + + return regions; +} + +static void top_surface_image_append_contoning_slices(TopSurfaceImageRegionPlan &plan, + const Layer &source_layer, + const ExPolygons &area, + const PrintObject &object, + const TextureMappingZone &zone, + const PrintConfig &print_config, + int depth, + const TextureMappingContoningSolver &solver, + TopSurfaceImageSourceSurface source_surface, + const ThrowIfCanceled *throw_if_canceled) +{ + if (area.empty() || !solver.valid()) + return; + check_canceled(throw_if_canceled); + + std::vector by_component(print_config.filament_colour.values.size() + 1); + const std::vector stack_regions = + top_surface_image_contoning_vector_regions(plan, + source_layer, + area, + object, + zone, + print_config, + depth, + solver, + source_surface, + throw_if_canceled); + for (const TopSurfaceImageContoningVectorRegion ®ion : stack_regions) { + check_canceled(throw_if_canceled); + if (depth < 0 || region.bottom_to_top.empty() || region.area.empty()) + continue; + const int pattern_depth = depth % int(region.bottom_to_top.size()); + const unsigned int component_id = + region.bottom_to_top[size_t(int(region.bottom_to_top.size()) - 1 - pattern_depth)]; + if (component_id == 0 || component_id >= by_component.size()) + continue; + append(by_component[component_id], region.area); + } + + ExPolygons depth_taken; + for (unsigned int component_id = 1; component_id < by_component.size(); ++component_id) { + check_canceled(throw_if_canceled); + if (by_component[component_id].empty()) + continue; + ExPolygons component_area = union_ex(by_component[component_id]); + if (!depth_taken.empty()) + component_area = diff_ex(component_area, depth_taken, ApplySafetyOffset::Yes); + if (component_area.empty()) + continue; + append(depth_taken, component_area); + TopSurfaceImageStackSlice slice; + slice.component_id = component_id; + slice.depth = depth; + slice.component_index = size_t(depth % std::max(1, std::min(plan.contoning_stack_layers, 4))); + slice.component_count = size_t(std::min(plan.contoning_stack_layers, 4)); + slice.contoning = true; + slice.lower_surface = source_surface == TopSurfaceImageSourceSurface::Bottom; + slice.angle_rad = (depth & 1) ? float(-PI / 4.0) : float(PI / 4.0); + slice.area = std::move(component_area); + plan.slices.emplace_back(std::move(slice)); + } +} + +static std::vector top_surface_image_region_plans(const Layer &layer, + const ThrowIfCanceled *throw_if_canceled) { std::vector plans(layer.regions().size()); const PrintObject *object = layer.object(); @@ -500,11 +1182,14 @@ static std::vector top_surface_image_region_plans(con return plans; Polygons current_layer_perimeters; - for (const LayerRegion *layerm : layer.regions()) + for (const LayerRegion *layerm : layer.regions()) { + check_canceled(throw_if_canceled); if (layerm != nullptr) layerm->perimeters.polygons_covered_by_width(current_layer_perimeters, 0.f); + } for (size_t region_id = 0; region_id < layer.regions().size(); ++region_id) { + check_canceled(throw_if_canceled); const LayerRegion *layerm = layer.regions()[region_id]; if (layerm == nullptr) continue; @@ -532,9 +1217,12 @@ static std::vector top_surface_image_region_plans(con plan.zone_id = zone_id; plan.same_layer_partition = zone->top_surface_image_printing_method == int(TextureMappingZone::TopSurfaceImageSameLayer45Partition); + plan.contoning = zone->top_surface_contoning_active(); plan.components_bottom_to_top = plan.same_layer_partition ? components : - top_surface_image_components_bottom_to_top(*zone, print_config, components); + (plan.contoning ? + texture_mapping_contoning_components_bottom_to_top(*zone, print_config, components) : + top_surface_image_components_bottom_to_top(*zone, print_config, components)); if (plan.components_bottom_to_top.empty()) continue; plan.max_width_mm = std::clamp(zone->top_surface_image_max_line_width_mm, @@ -544,49 +1232,106 @@ static std::vector top_surface_image_region_plans(con TextureMappingZone::MinTopSurfaceImageLineWidthMm, plan.max_width_mm); plan.fixed_coloring = zone->top_surface_image_fixed_coloring_filaments; + plan.color_lower_surfaces = zone->top_surface_contoning_color_lower_surfaces; plan.colored_top_layers = std::clamp(zone->top_surface_image_colored_top_layers, TextureMappingZone::MinTopSurfaceImageColoredTopLayers, TextureMappingZone::MaxTopSurfaceImageColoredTopLayers); + plan.contoning_stack_layers = std::clamp(zone->top_surface_contoning_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + plan.contoning_external_width_mm = float(layerm->flow(frExternalPerimeter).width()); + plan.contoning_min_feature_mm = + texture_mapping_contoning_min_feature_mm(*zone, print_config, components, plan.contoning_external_width_mm); + const TextureMappingContoningSolver contoning_solver(*zone, print_config, components); - const int stack_depth = plan.same_layer_partition ? + const int stack_depth = plan.contoning ? + plan.contoning_stack_layers : + (plan.same_layer_partition ? plan.colored_top_layers : - int(plan.components_bottom_to_top.size()); - for (int depth = 0; depth < stack_depth; ++depth) { - const Layer *top_layer = &layer; - for (int i = 0; i < depth && top_layer != nullptr; ++i) - top_layer = top_layer->upper_layer; - if (top_layer == nullptr) - break; + int(plan.components_bottom_to_top.size())); + if (plan.contoning) { + auto append_contoning_surface = [&](TopSurfaceImageSourceSurface source_surface) { + std::vector contoning_depth_layers(size_t(stack_depth), nullptr); + std::vector contoning_depth_areas(static_cast(stack_depth)); + for (int depth = 0; depth < stack_depth; ++depth) { + check_canceled(throw_if_canceled); + const Layer *source_layer = &layer; + for (int i = 0; i < depth && source_layer != nullptr; ++i) + source_layer = source_surface == TopSurfaceImageSourceSurface::Top ? + source_layer->upper_layer : + source_layer->lower_layer; + if (source_layer == nullptr) + break; - ExPolygons area = top_surface_image_visible_top_mask(*top_layer, zone_id); - if (area.empty()) - continue; - if (!current_layer_perimeters.empty()) - area = diff_ex(area, current_layer_perimeters, ApplySafetyOffset::Yes); - if (area.empty()) - continue; + ExPolygons area = top_surface_image_visible_surface_mask(*source_layer, zone_id, source_surface); + if (area.empty()) + continue; + if (!current_layer_perimeters.empty()) + area = diff_ex(area, current_layer_perimeters, ApplySafetyOffset::Yes); + if (area.empty()) + continue; + contoning_depth_layers[size_t(depth)] = source_layer; + contoning_depth_areas[size_t(depth)] = std::move(area); + } + for (int depth = 0; depth < stack_depth; ++depth) { + check_canceled(throw_if_canceled); + if (contoning_depth_layers[size_t(depth)] == nullptr || contoning_depth_areas[size_t(depth)].empty()) + continue; + top_surface_image_append_contoning_slices(plan, + *contoning_depth_layers[size_t(depth)], + contoning_depth_areas[size_t(depth)], + *object, + *zone, + print_config, + depth, + contoning_solver, + source_surface, + throw_if_canceled); + } + }; + append_contoning_surface(TopSurfaceImageSourceSurface::Top); + if (plan.color_lower_surfaces) + append_contoning_surface(TopSurfaceImageSourceSurface::Bottom); + } else { + for (int depth = 0; depth < stack_depth; ++depth) { + check_canceled(throw_if_canceled); + const Layer *top_layer = &layer; + for (int i = 0; i < depth && top_layer != nullptr; ++i) + top_layer = top_layer->upper_layer; + if (top_layer == nullptr) + break; - if (plan.same_layer_partition) { - const ExPolygons depth_area = area; - for (size_t component_idx = 0; component_idx < plan.components_bottom_to_top.size(); ++component_idx) { + ExPolygons area = top_surface_image_visible_top_mask(*top_layer, zone_id); + if (area.empty()) + continue; + if (!current_layer_perimeters.empty()) + area = diff_ex(area, current_layer_perimeters, ApplySafetyOffset::Yes); + if (area.empty()) + continue; + + if (plan.same_layer_partition) { + const ExPolygons depth_area = area; + for (size_t component_idx = 0; component_idx < plan.components_bottom_to_top.size(); ++component_idx) { + check_canceled(throw_if_canceled); + TopSurfaceImageStackSlice slice; + slice.component_id = plan.components_bottom_to_top[component_idx]; + slice.depth = depth; + slice.component_index = component_idx; + slice.component_count = plan.components_bottom_to_top.size(); + slice.same_layer_partition = true; + slice.angle_rad = (depth & 1) ? float(-PI / 4.0) : float(PI / 4.0); + slice.area = depth_area; + plan.slices.emplace_back(std::move(slice)); + } + } else { TopSurfaceImageStackSlice slice; - slice.component_id = plan.components_bottom_to_top[component_idx]; + slice.component_id = plan.components_bottom_to_top[size_t(stack_depth - 1 - depth)]; slice.depth = depth; - slice.component_index = component_idx; + slice.component_index = size_t(stack_depth - 1 - depth); slice.component_count = plan.components_bottom_to_top.size(); - slice.same_layer_partition = true; - slice.angle_rad = (depth & 1) ? float(-PI / 4.0) : float(PI / 4.0); - slice.area = depth_area; + slice.area = std::move(area); plan.slices.emplace_back(std::move(slice)); } - } else { - TopSurfaceImageStackSlice slice; - slice.component_id = plan.components_bottom_to_top[size_t(stack_depth - 1 - depth)]; - slice.depth = depth; - slice.component_index = size_t(stack_depth - 1 - depth); - slice.component_count = plan.components_bottom_to_top.size(); - slice.area = std::move(area); - plan.slices.emplace_back(std::move(slice)); } } @@ -631,6 +1376,13 @@ static void append_surface_fill_expolygons(SurfaceFill &fill, } } +static bool top_surface_image_slice_matches_surface(const TopSurfaceImageStackSlice &slice, const Surface &surface) +{ + return slice.lower_surface ? + (surface.surface_type == stBottom || surface.surface_type == stInternalSolid) : + (surface.is_top() || surface.surface_type == stInternalSolid); +} + static SurfaceFillParams top_surface_image_params_for_slice(const Layer &layer, const Surface &surface, const SurfaceFillParams &base_params, @@ -648,16 +1400,21 @@ static SurfaceFillParams top_surface_image_params_for_slice(const Layer &layer, params.multiline = 1; params.anchor_length = 1000.f; params.anchor_length_max = 1000.f; - params.extrusion_role = surface.is_top() ? erTopSolidInfill : erSolidInfill; + params.extrusion_role = surface.is_top() ? erTopSolidInfill : (surface.surface_type == stBottom ? erBottomSurface : erSolidInfill); const PrintConfig &print_config = layer.object()->print()->config(); const float nozzle = slice.component_id > 0 && size_t(slice.component_id - 1) < print_config.nozzle_diameter.values.size() ? float(print_config.nozzle_diameter.get_at(size_t(slice.component_id - 1))) : float(print_config.nozzle_diameter.values.empty() ? 0.4 : print_config.nozzle_diameter.values.front()); const float height = float((surface.thickness == -1) ? layer.height : surface.thickness); - params.flow = Flow(plan.max_width_mm, height, nozzle); - params.spacing = slice.same_layer_partition && slice.component_count > 0 ? - (plan.min_width_mm * float(slice.component_count) + (plan.max_width_mm - plan.min_width_mm)) : - params.flow.spacing(); + if (slice.contoning) { + params.flow = base_params.flow; + params.spacing = base_params.spacing; + } else { + params.flow = Flow(plan.max_width_mm, height, nozzle); + params.spacing = slice.same_layer_partition && slice.component_count > 0 ? + (plan.min_width_mm * float(slice.component_count) + (plan.max_width_mm - plan.min_width_mm)) : + params.flow.spacing(); + } params.texture_mapping_top_surface_image = true; params.texture_mapping_top_surface_zone_id = plan.zone_id; params.texture_mapping_top_surface_component_id = slice.component_id; @@ -666,6 +1423,7 @@ static SurfaceFillParams top_surface_image_params_for_slice(const Layer &layer, params.texture_mapping_top_surface_min_width_mm = plan.min_width_mm; params.texture_mapping_top_surface_max_width_mm = plan.max_width_mm; params.texture_mapping_top_surface_same_layer_partition = slice.same_layer_partition; + params.texture_mapping_top_surface_contoning = slice.contoning; params.texture_mapping_top_surface_component_index = int(slice.component_index); params.texture_mapping_top_surface_component_count = int(slice.component_count); return params; @@ -675,14 +1433,18 @@ static ExtrusionPaths top_surface_image_split_path(const ExtrusionPath &path, const TextureMappingOffsetContext &context, float min_width_mm, float max_width_mm, - float nozzle_diameter) + float nozzle_diameter, + const ThrowIfCanceled *throw_if_canceled) { ExtrusionPaths out; if (path.polyline.points.size() < 2) return out; + check_canceled(throw_if_canceled); const float height = path.height > 0.f ? path.height : context.layer_height_mm; const float sample_step_mm = std::clamp(max_width_mm * 0.5f, 0.16f, 0.40f); for (size_t point_idx = 1; point_idx < path.polyline.points.size(); ++point_idx) { + if ((point_idx & 63) == 1) + check_canceled(throw_if_canceled); const Point p0 = path.polyline.points[point_idx - 1]; const Point p1 = path.polyline.points[point_idx]; const double len_mm = unscale(p0.distance_to(p1)); @@ -725,10 +1487,11 @@ static ExtrusionEntity* top_surface_image_modulated_entity(const ExtrusionEntity const TextureMappingOffsetContext &context, float min_width_mm, float max_width_mm, - float nozzle_diameter) + float nozzle_diameter, + const ThrowIfCanceled *throw_if_canceled) { if (const ExtrusionPath *path = dynamic_cast(&entity)) { - ExtrusionPaths paths = top_surface_image_split_path(*path, context, min_width_mm, max_width_mm, nozzle_diameter); + ExtrusionPaths paths = top_surface_image_split_path(*path, context, min_width_mm, max_width_mm, nozzle_diameter, throw_if_canceled); if (paths.empty()) return entity.clone(); return new ExtrusionMultiPath(std::move(paths)); @@ -736,7 +1499,8 @@ static ExtrusionEntity* top_surface_image_modulated_entity(const ExtrusionEntity if (const ExtrusionMultiPath *multi_path = dynamic_cast(&entity)) { ExtrusionPaths paths; for (const ExtrusionPath &path : multi_path->paths) { - ExtrusionPaths split = top_surface_image_split_path(path, context, min_width_mm, max_width_mm, nozzle_diameter); + check_canceled(throw_if_canceled); + ExtrusionPaths split = top_surface_image_split_path(path, context, min_width_mm, max_width_mm, nozzle_diameter, throw_if_canceled); append(paths, std::move(split)); } if (paths.empty()) @@ -749,7 +1513,8 @@ static ExtrusionEntity* top_surface_image_modulated_entity(const ExtrusionEntity if (const ExtrusionLoop *loop = dynamic_cast(&entity)) { ExtrusionPaths paths; for (const ExtrusionPath &path : loop->paths) { - ExtrusionPaths split = top_surface_image_split_path(path, context, min_width_mm, max_width_mm, nozzle_diameter); + check_canceled(throw_if_canceled); + ExtrusionPaths split = top_surface_image_split_path(path, context, min_width_mm, max_width_mm, nozzle_diameter, throw_if_canceled); append(paths, std::move(split)); } if (paths.empty()) @@ -759,8 +1524,9 @@ static ExtrusionEntity* top_surface_image_modulated_entity(const ExtrusionEntity if (const ExtrusionEntityCollection *collection = dynamic_cast(&entity)) { ExtrusionEntityCollection *out = new ExtrusionEntityCollection(*collection); for (ExtrusionEntity *&child : out->entities) { + check_canceled(throw_if_canceled); ExtrusionEntity *replacement = - top_surface_image_modulated_entity(*child, context, min_width_mm, max_width_mm, nozzle_diameter); + top_surface_image_modulated_entity(*child, context, min_width_mm, max_width_mm, nozzle_diameter, throw_if_canceled); delete child; child = replacement; } @@ -803,8 +1569,10 @@ static std::vector top_surface_image_same_layer_fractions(const std::opti static void top_surface_image_same_layer_partition_fill(ExtrusionEntityCollection &collection, const Surface &surface, const SurfaceFillParams ¶ms, - const std::optional &context) + const std::optional &context, + const ThrowIfCanceled *throw_if_canceled) { + check_canceled(throw_if_canceled); int component_count = std::max(1, params.texture_mapping_top_surface_component_count); int component_index = std::clamp(params.texture_mapping_top_surface_component_index, 0, component_count - 1); if (context && int(context->component_ids.size()) == component_count) { @@ -852,9 +1620,11 @@ static void top_surface_image_same_layer_partition_fill(ExtrusionEntityCollectio const int band_end = int(std::ceil((max_v + pitch) / double(pitch))); const ExPolygons clip { surface.expolygon }; for (int band = band_start; band <= band_end; ++band) { + check_canceled(throw_if_canceled); const double band_v = double(band) * double(pitch); const double sample_v = band_v + double(pitch) * 0.5; for (double u0 = u_start; u0 < u_end - EPSILON; u0 += sample_step) { + check_canceled(throw_if_canceled); const double u1 = std::min(u0 + sample_step, u_end); const double um = 0.5 * (u0 + u1); const double sample_x = um * cos_t - sample_v * sin_t; @@ -898,8 +1668,10 @@ static void top_surface_image_same_layer_partition_fill(ExtrusionEntityCollectio static void apply_top_surface_image_collection_metadata(ExtrusionEntityCollection &collection, const SurfaceFillParams ¶ms, - const std::optional &context) + const std::optional &context, + const ThrowIfCanceled *throw_if_canceled) { + check_canceled(throw_if_canceled); collection.texture_mapping_top_surface_image = true; collection.texture_mapping_top_surface_zone_id = params.texture_mapping_top_surface_zone_id; collection.texture_mapping_top_surface_desired_component_id = params.texture_mapping_top_surface_component_id; @@ -908,16 +1680,19 @@ static void apply_top_surface_image_collection_metadata(ExtrusionEntityCollectio if (params.texture_mapping_top_surface_fixed_coloring && params.texture_mapping_top_surface_component_id > 0) collection.texture_mapping_extruder_override = int(params.texture_mapping_top_surface_component_id - 1); - if (!context) + if (!context || params.texture_mapping_top_surface_contoning) return; ExtrusionEntitiesPtr replacement; replacement.reserve(collection.entities.size()); - for (const ExtrusionEntity *entity : collection.entities) + for (const ExtrusionEntity *entity : collection.entities) { + check_canceled(throw_if_canceled); replacement.emplace_back(top_surface_image_modulated_entity(*entity, *context, params.texture_mapping_top_surface_min_width_mm, params.texture_mapping_top_surface_max_width_mm, - params.flow.nozzle_diameter())); + params.flow.nozzle_diameter(), + throw_if_canceled)); + } collection.clear(); collection.entities = std::move(replacement); } @@ -1403,8 +2178,9 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n #endif } -std::vector group_fills(const Layer &layer, LockRegionParam &lock_param) +std::vector group_fills(const Layer &layer, LockRegionParam &lock_param, const ThrowIfCanceled *throw_if_canceled = nullptr) { + check_canceled(throw_if_canceled); std::vector surface_fills; // Fill in a map of a region & surface to SurfaceFillParams. std::set set_surface_params; @@ -1412,7 +2188,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p SurfaceFillParams params; bool has_internal_voids = false; const PrintObjectConfig& object_config = layer.object()->config(); - const std::vector top_surface_plans = top_surface_image_region_plans(layer); + const std::vector top_surface_plans = top_surface_image_region_plans(layer, throw_if_canceled); auto append_flow_param = [](std::map &flow_params, Flow flow, const ExPolygon &exp) { auto it = flow_params.find(flow); @@ -1431,6 +2207,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p }; for (size_t region_id = 0; region_id < layer.regions().size(); ++ region_id) { + check_canceled(throw_if_canceled); const LayerRegion &layerm = *layer.regions()[region_id]; region_to_surface_params[region_id].assign(layerm.fill_surfaces.size(), nullptr); for (const Surface &surface : layerm.fill_surfaces.surfaces) @@ -1579,11 +2356,13 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p surface_fills.reserve(set_surface_params.size()); for (const SurfaceFillParams ¶ms : set_surface_params) { + check_canceled(throw_if_canceled); const_cast(params).idx = surface_fills.size(); surface_fills.emplace_back(params); } for (size_t region_id = 0; region_id < layer.regions().size(); ++ region_id) { + check_canceled(throw_if_canceled); const LayerRegion &layerm = *layer.regions()[region_id]; for (const Surface &surface : layerm.fill_surfaces.surfaces) if (surface.surface_type != stInternalVoid) { @@ -1592,32 +2371,48 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p ExPolygons remaining = { surface.expolygon }; if (region_id < top_surface_plans.size() && top_surface_plans[region_id].zone != nullptr && - (surface.is_top() || surface.surface_type == stInternalSolid) && + (surface.is_top() || surface.surface_type == stInternalSolid || surface.surface_type == stBottom) && !surface.is_bridge()) { const TopSurfaceImageRegionPlan &plan = top_surface_plans[region_id]; for (size_t slice_idx = 0; slice_idx < plan.slices.size();) { + check_canceled(throw_if_canceled); const TopSurfaceImageStackSlice &slice = plan.slices[slice_idx]; + if (!top_surface_image_slice_matches_surface(slice, surface)) { + ++slice_idx; + continue; + } if (remaining.empty()) break; ExPolygons image_expolygons = intersection_ex(remaining, slice.area, ApplySafetyOffset::Yes); - if (image_expolygons.empty()) { + if (image_expolygons.empty() && !slice.contoning) { ++slice_idx; continue; } ExPolygons image_clip = image_expolygons; - if (slice.same_layer_partition) { + if (slice.same_layer_partition || slice.contoning) { size_t same_depth_end = slice_idx; while (same_depth_end < plan.slices.size() && - plan.slices[same_depth_end].same_layer_partition && + plan.slices[same_depth_end].same_layer_partition == slice.same_layer_partition && + plan.slices[same_depth_end].contoning == slice.contoning && + plan.slices[same_depth_end].lower_surface == slice.lower_surface && plan.slices[same_depth_end].depth == slice.depth) ++same_depth_end; + ExPolygons depth_clip; for (size_t same_idx = slice_idx; same_idx < same_depth_end; ++same_idx) { + check_canceled(throw_if_canceled); SurfaceFillParams image_params = top_surface_image_params_for_slice(layer, surface, *params, plan, plan.slices[same_idx]); - ExPolygons component_expolygons = image_expolygons; - SurfaceFill &image_fill = surface_fill_for_params(surface_fills, image_params); - append_surface_fill_expolygons(image_fill, region_id, surface, std::move(component_expolygons), layerm); + ExPolygons component_expolygons = slice.same_layer_partition ? + image_expolygons : + intersection_ex(remaining, plan.slices[same_idx].area, ApplySafetyOffset::Yes); + if (!component_expolygons.empty()) { + append(depth_clip, component_expolygons); + SurfaceFill &image_fill = surface_fill_for_params(surface_fills, image_params); + append_surface_fill_expolygons(image_fill, region_id, surface, std::move(component_expolygons), layerm); + } } + if (!depth_clip.empty()) + image_clip = union_ex(depth_clip); slice_idx = same_depth_end; } else { SurfaceFillParams image_params = @@ -1641,6 +2436,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p Polygons all_polygons; for (SurfaceFill &fill : surface_fills) if (! fill.expolygons.empty()) { + check_canceled(throw_if_canceled); if (fill.params.texture_mapping_top_surface_same_layer_partition) continue; if (fill.expolygons.size() > 1 || ! all_polygons.empty()) { @@ -1664,6 +2460,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p // TODO: detect and investigate whether there could be narrow regions without // any void neighbors if (has_internal_voids) { + check_canceled(throw_if_canceled); // Internal voids are generated only if "infill_only_where_needed" or "infill_every_layers" are active. coord_t distance_between_surfaces = 0; Polygons surfaces_polygons; @@ -1673,6 +2470,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p int region_some_infill = -1; for (SurfaceFill &surface_fill : surface_fills) if (! surface_fill.expolygons.empty()) { + check_canceled(throw_if_canceled); distance_between_surfaces = std::max(distance_between_surfaces, surface_fill.params.flow.scaled_spacing()); append((surface_fill.surface.surface_type == stInternalVoid) ? voids : surfaces_polygons, to_polygons(surface_fill.expolygons)); if (surface_fill.surface.surface_type == stInternalSolid) @@ -1683,6 +2481,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p region_some_infill = (int)surface_fill.region_id; } if (! voids.empty() && ! surfaces_polygons.empty()) { + check_canceled(throw_if_canceled); // First clip voids by the printing polygons, as the voids were ignored by the loop above during mutual clipping. voids = diff(voids, surfaces_polygons); // Corners of infill regions, which would not be filled with an extrusion path with a radius of distance_between_surfaces/2 @@ -1743,6 +2542,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p if (layer.object()->config().detect_narrow_internal_solid_infill) { size_t surface_fills_size = surface_fills.size(); for (size_t i = 0; i < surface_fills_size; i++) { + check_canceled(throw_if_canceled); if (surface_fills[i].surface.surface_type != stInternalSolid || surface_fills[i].params.texture_mapping_top_surface_image) continue; @@ -1801,8 +2601,13 @@ void export_group_fills_to_svg(const char *path, const std::vector #endif // friend to Layer -void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator) +void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, + FillAdaptive::Octree* support_fill_octree, + FillLightning::Generator* lightning_generator, + std::function throw_if_canceled) { + const ThrowIfCanceled *throw_if_canceled_ptr = throw_if_canceled ? &throw_if_canceled : nullptr; + check_canceled(throw_if_canceled_ptr); for (LayerRegion *layerm : m_regions) layerm->fills.clear(); @@ -1811,9 +2616,10 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // this->export_region_fill_surfaces_to_svg_debug("10_fill-initial"); #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ LockRegionParam lock_param; - std::vector surface_fills = group_fills(*this, lock_param); + std::vector surface_fills = group_fills(*this, lock_param, throw_if_canceled_ptr); const Slic3r::BoundingBox bbox = this->object()->bounding_box(); const auto resolution = this->object()->print()->config().resolution.value; + check_canceled(throw_if_canceled_ptr); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { @@ -1823,6 +2629,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ for (SurfaceFill &surface_fill : surface_fills) { + check_canceled(throw_if_canceled_ptr); // Create the filler object. std::unique_ptr f = std::unique_ptr(Fill::new_from_type(surface_fill.params.pattern)); f->set_bounding_box(bbox); @@ -1867,6 +2674,12 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // apply half spacing using this flow's own spacing and generate infill FillParams params; + if (throw_if_canceled_ptr != nullptr) { + params.throw_if_canceled = [](void *context) { + (*static_cast(context))(); + }; + params.throw_if_canceled_context = const_cast(throw_if_canceled_ptr); + } params.density = float(0.01 * surface_fill.params.density); params.multiline = surface_fill.params.multiline; params.dont_adjust = false; // surface_fill.params.dont_adjust; @@ -1911,7 +2724,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.can_reverse = false; std::optional top_surface_image_context; - if (surface_fill.params.texture_mapping_top_surface_image) { + if (surface_fill.params.texture_mapping_top_surface_image && + !surface_fill.params.texture_mapping_top_surface_contoning) { + check_canceled(throw_if_canceled_ptr); const Print *print = this->object()->print(); const TextureMappingZone *zone = print != nullptr ? print->texture_mapping_manager().zone_from_id(surface_fill.params.texture_mapping_top_surface_zone_id) : @@ -1941,8 +2756,10 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: } for (ExPolygon& expoly : surface_fill.expolygons) { + check_canceled(throw_if_canceled_ptr); f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); + check_canceled(throw_if_canceled_ptr); if (params.symmetric_infill_y_axis) { params.symmetric_y_axis = f->extended_object_bounding_box().center().x(); expoly.symmetric_y(params.symmetric_y_axis); @@ -1970,16 +2787,15 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // make fill ExtrusionEntitiesPtr &fill_entities = m_regions[surface_fill.region_id]->fills.entities; if (surface_fill.params.texture_mapping_top_surface_same_layer_partition) { - ExtrusionEntityCollection *collection = new ExtrusionEntityCollection(); + std::unique_ptr collection(new ExtrusionEntityCollection()); top_surface_image_same_layer_partition_fill(*collection, surface_fill.surface, surface_fill.params, - top_surface_image_context); + top_surface_image_context, + throw_if_canceled_ptr); if (!collection->empty()) { - apply_top_surface_image_collection_metadata(*collection, surface_fill.params, std::nullopt); - fill_entities.push_back(collection); - } else { - delete collection; + apply_top_surface_image_collection_metadata(*collection, surface_fill.params, std::nullopt, throw_if_canceled_ptr); + fill_entities.push_back(collection.release()); } } else { const size_t fill_entities_before = fill_entities.size(); @@ -1989,7 +2805,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: if (surface_fill.params.texture_mapping_top_surface_image) { for (size_t i = fill_entities_before; i < fill_entities.size(); ++i) if (ExtrusionEntityCollection *collection = dynamic_cast(fill_entities[i])) - apply_top_surface_image_collection_metadata(*collection, surface_fill.params, top_surface_image_context); + apply_top_surface_image_collection_metadata(*collection, surface_fill.params, top_surface_image_context, throw_if_canceled_ptr); } } } @@ -2001,6 +2817,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // Why the paths are unpacked? for (LayerRegion *layerm : m_regions) for (const ExtrusionEntity *thin_fill : layerm->thin_fills.entities) { + check_canceled(throw_if_canceled_ptr); ExtrusionEntityCollection &collection = *(new ExtrusionEntityCollection()); if (const auto *thin_fill_collection = dynamic_cast(thin_fill)) { collection.texture_mapping_extruder_override = thin_fill_collection->texture_mapping_extruder_override; diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index eff83efe3ec..0635b03314a 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -104,28 +104,38 @@ bool Fill::use_bridge_flow(const InfillPattern type) Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms) { + params.check_canceled(); // Perform offset. Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing))); + params.check_canceled(); // Create the infills for each of the regions. Polylines polylines_out; - for (size_t i = 0; i < expp.size(); ++ i) + for (size_t i = 0; i < expp.size(); ++ i) { + params.check_canceled(); _fill_surface_single( params, surface->thickness_layers, _infill_direction(surface), std::move(expp[i]), polylines_out); + } + params.check_canceled(); return polylines_out; } ThickPolylines Fill::fill_surface_arachne(const Surface* surface, const FillParams& params) { + params.check_canceled(); // Perform offset. Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing))); + params.check_canceled(); // Create the infills for each of the regions. ThickPolylines thick_polylines_out; - for (ExPolygon& expoly : expp) + for (ExPolygon& expoly : expp) { + params.check_canceled(); _fill_surface_single(params, surface->thickness_layers, _infill_direction(surface), std::move(expoly), thick_polylines_out); + } + params.check_canceled(); return thick_polylines_out; } @@ -135,14 +145,17 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para Polylines polylines; ThickPolylines thick_polylines; try { + params.check_canceled(); if (params.use_arachne) thick_polylines = this->fill_surface_arachne(surface, params); else polylines = this->fill_surface(surface, params); + params.check_canceled(); } catch (InfillFailedException&) {} if (!polylines.empty() || !thick_polylines.empty()) { + params.check_canceled(); // calculate actual flow from spacing (which might have been adjusted by the infill // pattern generator) double flow_mm3_per_mm = params.flow.mm3_per_mm(); @@ -187,6 +200,7 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para // Orca: run gap fill this->_create_gap_fill(surface, params, eec); + params.check_canceled(); } } @@ -194,6 +208,7 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para // and append them to the out ExtrusionEntityCollection. void Fill::_create_gap_fill(const Surface* surface, const FillParams& params, ExtrusionEntityCollection* out){ + params.check_canceled(); //Orca: just to be safe, check against null pointer for the print object config and if NULL return. if (this->print_object_config == nullptr) return; @@ -205,9 +220,11 @@ void Fill::_create_gap_fill(const Surface* surface, const FillParams& params, Ex Flow new_flow = params.flow; ExPolygons unextruded_areas; unextruded_areas = diff_ex(this->no_overlap_expolygons, union_ex(out->polygons_covered_by_spacing(10))); + params.check_canceled(); ExPolygons gapfill_areas = union_ex(unextruded_areas); if (!this->no_overlap_expolygons.empty()) gapfill_areas = intersection_ex(gapfill_areas, this->no_overlap_expolygons); + params.check_canceled(); if (gapfill_areas.size() > 0 && params.density >= 1) { double min = 0.2 * new_flow.scaled_spacing() * (1 - INSET_OVERLAP_TOLERANCE); @@ -215,6 +232,7 @@ void Fill::_create_gap_fill(const Surface* surface, const FillParams& params, Ex ExPolygons gaps_ex = diff_ex( opening_ex(gapfill_areas, float(min / 2.)), offset2_ex(gapfill_areas, -float(max / 2.), float(max / 2. + ClipperSafetyOffset))); + params.check_canceled(); //BBS: sort the gap_ex to avoid mess travel Points ordering_points; ordering_points.reserve(gaps_ex.size()); @@ -228,10 +246,12 @@ void Fill::_create_gap_fill(const Surface* surface, const FillParams& params, Ex ThickPolylines polylines; for (ExPolygon& ex : gaps_ex_sorted) { + params.check_canceled(); //BBS: Use DP simplify to avoid duplicated points and accelerate medial-axis calculation as well. ex.douglas_peucker(SCALED_RESOLUTION * 0.1); ex.medial_axis(min, max, &polylines); } + params.check_canceled(); if (!polylines.empty() && !is_bridge(params.extrusion_role)) { polylines.erase(std::remove_if(polylines.begin(), polylines.end(), @@ -243,6 +263,7 @@ void Fill::_create_gap_fill(const Surface* surface, const FillParams& params, Ex variable_width(polylines, erGapFill, params.flow, gap_fill.entities); auto gap = std::move(gap_fill.entities); out->append(gap); + params.check_canceled(); } } } @@ -1240,6 +1261,7 @@ void mark_boundary_segments_touching_infill( void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_src, Polylines &polylines_out, const double spacing, const FillParams ¶ms) { + params.check_canceled(); assert(! boundary_src.contour.points.empty()); auto polygons_src = reserve_vector(boundary_src.holes.size() + 1); polygons_src.emplace_back(&boundary_src.contour); @@ -1251,6 +1273,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_ void Fill::connect_infill(Polylines &&infill_ordered, const Polygons &boundary_src, const BoundingBox &bbox, Polylines &polylines_out, const double spacing, const FillParams ¶ms) { + params.check_canceled(); auto polygons_src = reserve_vector(boundary_src.size()); for (const Polygon &polygon : boundary_src) polygons_src.emplace_back(&polygon); @@ -1577,6 +1600,7 @@ BoundingBox Fill::extended_object_bounding_box() const void Fill::connect_infill(Polylines &&infill_ordered, const std::vector &boundary_src, const BoundingBox &bbox, Polylines &polylines_out, const double spacing, const FillParams ¶ms) { + params.check_canceled(); assert(! infill_ordered.empty()); assert(params.anchor_length >= 0.); assert(params.anchor_length_max >= 0.01f); @@ -1590,6 +1614,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector merged_with(infill_ordered.size()); std::iota(merged_with.begin(), merged_with.end(), 0); @@ -1699,7 +1724,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vectorconsumed && ! arc.intersection->next_on_contour->consumed) { // Indices of the polylines to be connected by a perimeter segment. ContourIntersectionPoint *cp1 = arc.intersection; @@ -1742,9 +1770,13 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector &contour_params = graph.boundary_params[contour_point.contour_idx]; @@ -1808,15 +1840,18 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector 1) diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index 010dff8a086..c84738312cc 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -50,6 +50,7 @@ struct FillParams bool full_infill() const { return density > 0.9999f; } // Don't connect the fill lines around the inner perimeter. bool dont_connect() const { return anchor_length_max < 0.05f; } + void check_canceled() const { if (throw_if_canceled != nullptr) throw_if_canceled(throw_if_canceled_context); } // Fill density, fraction in <0, 1> float density { 0.f }; @@ -103,6 +104,8 @@ struct FillParams bool locked_zag{false}; float infill_lock_depth{0.0}; float skin_infill_depth{0.0}; + void (*throw_if_canceled)(void*){ nullptr }; + void *throw_if_canceled_context{ nullptr }; }; static_assert(IsTriviallyCopyable::value, "FillParams class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 0445cbc6fe8..cef289a56df 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -7,6 +7,7 @@ #include "SurfaceCollection.hpp" #include "ExtrusionEntityCollection.hpp" #include "BoundingBox.hpp" +#include namespace Slic3r { class ExPolygon; @@ -193,7 +194,10 @@ public: void make_perimeters(); // Phony version of make_fills() without parameters for Perl integration only. void make_fills() { this->make_fills(nullptr, nullptr); } - void make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator = nullptr); + void make_fills(FillAdaptive::Octree* adaptive_fill_octree, + FillAdaptive::Octree* support_fill_octree, + FillLightning::Generator* lightning_generator = nullptr, + std::function throw_if_canceled = {}); Polylines generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree *adaptive_fill_octree, FillAdaptive::Octree *support_fill_octree, FillLightning::Generator* lightning_generator) const; diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index 5a0b70209ce..15e6595997f 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -9,6 +9,7 @@ #include "BoundingBox.hpp" #include "SVG.hpp" #include "TextureMapping.hpp" +#include "TextureMappingContoning.hpp" #include "TextureMappingOffset.hpp" #include "MultiMaterialSegmentation.hpp" #include "Algorithm/RegionExpansion.hpp" @@ -452,9 +453,13 @@ static std::optional perimeter_texture_choose_gradient_recolor_com struct PerimeterTextureRecolorSampler { bool image_texture { false }; + bool contoning { false }; size_t num_physical { 0 }; std::optional image_context; std::vector> image_component_colors; + TextureMappingContoningSolver contoning_solver; + int contoning_stack_layers { TextureMappingZone::DefaultTopSurfaceContoningStackLayers }; + float contoning_angle_threshold_deg { TextureMappingZone::DefaultTopSurfaceContoningAngleThresholdDeg }; std::vector component_ids; std::vector gradient_contexts; }; @@ -502,6 +507,21 @@ static std::optional perimeter_texture_make_reco sampler.image_component_colors.push_back({ 0.f, 0.f, 0.f }); } } + if (zone.top_surface_contoning_perimeters_active()) { + sampler.contoning = true; + sampler.contoning_solver = + TextureMappingContoningSolver(zone, print_config, sampler.image_context->component_ids); + sampler.contoning_stack_layers = + std::clamp(zone.top_surface_contoning_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + sampler.contoning_angle_threshold_deg = + std::clamp(zone.top_surface_contoning_angle_threshold_deg, + TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg, + TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg); + if (!sampler.contoning_solver.valid()) + sampler.contoning = false; + } return sampler; } @@ -532,6 +552,32 @@ static std::optional perimeter_texture_choose_recolor_component_wi if (sampler.image_texture) { if (!sampler.image_context) return std::nullopt; + if (sampler.contoning) { + std::optional> rgb = + perimeter_texture_average_visible_image_rgb(visible, *sampler.image_context); + if (!rgb && fallback_entity != nullptr) { + std::vector accum(sampler.image_context->component_ids.size(), 0.0); + double total_weight = 0.0; + perimeter_texture_accumulate_path_image_weights(*fallback_entity, *sampler.image_context, accum, total_weight); + if (total_weight > EPSILON) { + std::array fallback_rgb{ 0.f, 0.f, 0.f }; + for (size_t idx = 0; idx < accum.size() && idx < sampler.image_component_colors.size(); ++idx) { + const double w = accum[idx] / total_weight; + fallback_rgb[0] += float(sampler.image_component_colors[idx][0] * w); + fallback_rgb[1] += float(sampler.image_component_colors[idx][1] * w); + fallback_rgb[2] += float(sampler.image_component_colors[idx][2] * w); + } + rgb = fallback_rgb; + } + } + if (!rgb) + return std::nullopt; + const unsigned int component_id = + sampler.contoning_solver.component_for_depth(*rgb, sampler.contoning_stack_layers, 0); + if (component_id >= 1 && component_id <= sampler.num_physical) + return component_id; + return std::nullopt; + } std::optional chosen = perimeter_texture_choose_image_recolor_component(visible, fallback_entity, @@ -940,7 +986,9 @@ static void perimeter_texture_sample_segment_visible_points(float static std::optional perimeter_texture_choose_recolor_component_from_point_samples( const std::vector &samples, - const PerimeterTextureRecolorSampler &sampler) + const PerimeterTextureRecolorSampler &sampler, + int depth_from_top = 0, + int contoning_stack_layers = 0) { if (samples.empty()) return std::nullopt; @@ -948,6 +996,30 @@ static std::optional perimeter_texture_choose_recolor_component_fr if (sampler.image_texture) { if (!sampler.image_context) return std::nullopt; + if (sampler.contoning) { + std::array rgb_accum{ 0.0, 0.0, 0.0 }; + double rgb_total_weight = 0.0; + for (const PerimeterTextureVisiblePointSample &sample : samples) { + perimeter_texture_accumulate_image_rgb_at_point(*sampler.image_context, sample.point, sample.weight, rgb_accum, rgb_total_weight); + } + if (rgb_total_weight <= EPSILON) + return std::nullopt; + const std::array rgb{ + float(std::clamp(rgb_accum[0] / rgb_total_weight, 0.0, 1.0)), + float(std::clamp(rgb_accum[1] / rgb_total_weight, 0.0, 1.0)), + float(std::clamp(rgb_accum[2] / rgb_total_weight, 0.0, 1.0)) + }; + const int stack_layers = contoning_stack_layers > 0 ? + std::clamp(contoning_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + sampler.contoning_stack_layers) : + sampler.contoning_stack_layers; + const unsigned int component_id = + sampler.contoning_solver.component_for_depth(rgb, stack_layers, depth_from_top); + if (component_id >= 1 && component_id <= sampler.num_physical) + return component_id; + return std::nullopt; + } if (!sampler.image_context->weight_field.raw_component_weights_from_texture) { std::array rgb_accum{ 0.0, 0.0, 0.0 }; double rgb_total_weight = 0.0; @@ -1425,6 +1497,9 @@ struct PerimeterTextureRecolorEntityPiece { struct PerimeterTexturePathRecolorContext { PerimeterTextureMaskIndex path_mask; const PerimeterTextureRecolorSampler *sampler { nullptr }; + float min_recolor_run_length_mm { 0.f }; + int path_depth_from_top { 0 }; + int contoning_stack_layers { TextureMappingZone::DefaultTopSurfaceContoningStackLayers }; }; struct PerimeterTexturePathSegment { @@ -1639,23 +1714,22 @@ static void perimeter_texture_clear_short_recolor_runs(std::vector &visible_samples) { if (context.sampler == nullptr || context.path_mask.empty() || a == b) - return 0; + return; const double dx = double(b.x()) - double(a.x()); const double dy = double(b.y()) - double(a.y()); const double len = std::hypot(dx, dy); if (!std::isfinite(len) || len <= EPSILON) - return 0; + return; const double inward_x = -dy / len; const double inward_y = dx / len; - std::vector visible_samples; - visible_samples.reserve(3); const std::array sample_points{ perimeter_texture_interpolate_point(a, b, 0.25), perimeter_texture_interpolate_point(a, b, 0.50), @@ -1664,9 +1738,24 @@ static unsigned int perimeter_texture_recolor_component_for_segment(const Perime for (const Point &sample : sample_points) if (perimeter_texture_mask_index_contains_point(context.path_mask, sample)) visible_samples.push_back(PerimeterTextureVisiblePointSample{ sample, 1.0, inward_x, inward_y }); +} + +static unsigned int perimeter_texture_recolor_component_for_segment(const PerimeterTexturePathRecolorContext &context, + const Point &a, + const Point &b) +{ + if (context.sampler == nullptr) + return 0; + + std::vector visible_samples; + visible_samples.reserve(3); + perimeter_texture_collect_visible_samples_for_segment(context, a, b, visible_samples); const std::optional component = - perimeter_texture_choose_recolor_component_from_point_samples(visible_samples, *context.sampler); + perimeter_texture_choose_recolor_component_from_point_samples(visible_samples, + *context.sampler, + context.path_depth_from_top, + context.contoning_stack_layers); return component && *component > 0 ? *component : 0; } @@ -1691,13 +1780,94 @@ static void perimeter_texture_append_recolor_path_piece(std::vector(polyline.length()) + unscale(std::max(1.0, double(SCALED_EPSILON))) < 2.0 * double(source.width)) extruder_override = -1; - pieces.push_back(PerimeterTextureRecolorEntityPiece{ extruder_override, new ExtrusionPath(std::move(polyline), source) }); + ExtrusionPath *path = new ExtrusionPath(std::move(polyline), source); + path->inset_idx = source.inset_idx; + pieces.push_back(PerimeterTextureRecolorEntityPiece{ extruder_override, path }); +} + +static bool perimeter_texture_entity_has_recolorable_path(const ExtrusionEntity &entity) +{ + if (const ExtrusionPath *path = dynamic_cast(&entity)) + return perimeter_texture_path_role_can_top_visible_recolor(path->role()); + if (const ExtrusionMultiPath *multipath = dynamic_cast(&entity)) + return std::any_of(multipath->paths.begin(), multipath->paths.end(), [](const ExtrusionPath &path) { + return perimeter_texture_path_role_can_top_visible_recolor(path.role()); + }); + if (const ExtrusionLoop *loop = dynamic_cast(&entity)) + return std::any_of(loop->paths.begin(), loop->paths.end(), [](const ExtrusionPath &path) { + return perimeter_texture_path_role_can_top_visible_recolor(path.role()); + }); + if (const ExtrusionEntityCollection *collection = dynamic_cast(&entity)) + return std::any_of(collection->entities.begin(), collection->entities.end(), [](const ExtrusionEntity *child) { + return child != nullptr && perimeter_texture_entity_has_recolorable_path(*child); + }); + return false; +} + +static int perimeter_texture_entity_depth_from_top(const ExtrusionEntity &entity, int fallback_depth) +{ + if (entity.inset_idx >= 0) + return entity.inset_idx; + if (is_external_perimeter(entity.role())) + return 0; + if (is_internal_perimeter(entity.role()) || entity.role() == erOverhangPerimeter) + return std::max(1, fallback_depth); + return std::max(0, fallback_depth); +} + +static void perimeter_texture_collect_recolorable_inset_depths(const ExtrusionEntity &entity, int &max_inset_idx) +{ + if (const ExtrusionPath *path = dynamic_cast(&entity)) { + if (perimeter_texture_path_role_can_top_visible_recolor(path->role()) && path->inset_idx >= 0) + max_inset_idx = std::max(max_inset_idx, path->inset_idx); + return; + } + if (const ExtrusionMultiPath *multipath = dynamic_cast(&entity)) { + if (perimeter_texture_path_role_can_top_visible_recolor(multipath->role()) && multipath->inset_idx >= 0) + max_inset_idx = std::max(max_inset_idx, multipath->inset_idx); + for (const ExtrusionPath &path : multipath->paths) + if (perimeter_texture_path_role_can_top_visible_recolor(path.role()) && path.inset_idx >= 0) + max_inset_idx = std::max(max_inset_idx, path.inset_idx); + return; + } + if (const ExtrusionLoop *loop = dynamic_cast(&entity)) { + if (perimeter_texture_path_role_can_top_visible_recolor(loop->role()) && loop->inset_idx >= 0) + max_inset_idx = std::max(max_inset_idx, loop->inset_idx); + for (const ExtrusionPath &path : loop->paths) + if (perimeter_texture_path_role_can_top_visible_recolor(path.role()) && path.inset_idx >= 0) + max_inset_idx = std::max(max_inset_idx, path.inset_idx); + return; + } + if (const ExtrusionEntityCollection *collection = dynamic_cast(&entity)) { + for (const ExtrusionEntity *child : collection->entities) + if (child != nullptr) + perimeter_texture_collect_recolorable_inset_depths(*child, max_inset_idx); + } +} + +static int perimeter_texture_available_contoning_shell_slots(const ExtrusionEntityCollection &perimeters, + int fallback_wall_loops, + int configured_stack_layers) +{ + int max_inset_idx = -1; + for (const ExtrusionEntity *entity : perimeters.entities) + if (entity != nullptr) + perimeter_texture_collect_recolorable_inset_depths(*entity, max_inset_idx); + const int configured_layers = + std::clamp(configured_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + const int available_slots = max_inset_idx >= 0 ? max_inset_idx + 1 : std::max(1, fallback_wall_loops); + return std::clamp(available_slots, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + configured_layers); } static bool perimeter_texture_split_path_by_recolor_masks(const ExtrusionPath &path, const PerimeterTexturePathRecolorContext &context, std::vector &pieces, - bool emit_unchanged) + bool emit_unchanged, + int path_depth_from_top) { if (!perimeter_texture_path_role_can_top_visible_recolor(path.role()) || path.polyline.points.size() < 2) { if (emit_unchanged) @@ -1707,6 +1877,10 @@ static bool perimeter_texture_split_path_by_recolor_masks(const ExtrusionPath std::vector path_segments; path_segments.reserve(path.polyline.points.size() - 1); + std::vector> segment_visible_samples; + const bool use_chunked_contoning = context.sampler != nullptr && context.sampler->contoning; + if (use_chunked_contoning) + segment_visible_samples.reserve(path.polyline.points.size() - 1); bool saw_segment = false; bool saw_override = false; @@ -1716,13 +1890,82 @@ static bool perimeter_texture_split_path_by_recolor_masks(const ExtrusionPath if (a == b) continue; saw_segment = true; - const unsigned int component = perimeter_texture_recolor_component_for_segment(context, a, b); - const int extruder_override = component > 0 ? int(component) - 1 : -1; + PerimeterTexturePathRecolorContext segment_context = context; + segment_context.path_depth_from_top = path_depth_from_top; + int extruder_override = -1; + if (use_chunked_contoning) { + segment_visible_samples.emplace_back(); + segment_visible_samples.back().reserve(3); + perimeter_texture_collect_visible_samples_for_segment(segment_context, a, b, segment_visible_samples.back()); + } else { + const unsigned int component = perimeter_texture_recolor_component_for_segment(segment_context, a, b); + extruder_override = component > 0 ? int(component) - 1 : -1; + } if (extruder_override >= 0) saw_override = true; path_segments.push_back(PerimeterTexturePathSegment{ a, b, extruder_override }); } + if (use_chunked_contoning && context.sampler != nullptr) { + PerimeterTexturePathRecolorContext segment_context = context; + segment_context.path_depth_from_top = path_depth_from_top; + const double min_chunk_length_scaled = + double(scale_(std::max(std::isfinite(path.width) && path.width > 0.f ? 2.f * path.width : 0.f, + context.min_recolor_run_length_mm))); + for (size_t start = 0; start < path_segments.size();) { + while (start < path_segments.size() && + (start >= segment_visible_samples.size() || segment_visible_samples[start].empty())) + ++start; + if (start >= path_segments.size()) + break; + + size_t run_end = start; + while (run_end < path_segments.size() && + run_end < segment_visible_samples.size() && + !segment_visible_samples[run_end].empty()) + ++run_end; + + for (size_t chunk_start = start; chunk_start < run_end;) { + size_t chunk_end = chunk_start; + double chunk_length_scaled = 0.0; + std::vector chunk_samples; + while (chunk_end < run_end && + (chunk_end == chunk_start || chunk_length_scaled < min_chunk_length_scaled)) { + chunk_length_scaled += perimeter_texture_path_segment_length_scaled(path_segments[chunk_end]); + chunk_samples.insert(chunk_samples.end(), + segment_visible_samples[chunk_end].begin(), + segment_visible_samples[chunk_end].end()); + ++chunk_end; + } + if (chunk_end < run_end) { + const double remaining_length_scaled = + perimeter_texture_path_segments_length_scaled(path_segments, chunk_end, run_end); + if (remaining_length_scaled < min_chunk_length_scaled * 0.5) { + for (; chunk_end < run_end; ++chunk_end) { + chunk_samples.insert(chunk_samples.end(), + segment_visible_samples[chunk_end].begin(), + segment_visible_samples[chunk_end].end()); + } + } + } + const std::optional component = + perimeter_texture_choose_recolor_component_from_point_samples(chunk_samples, + *context.sampler, + segment_context.path_depth_from_top, + segment_context.contoning_stack_layers); + const int extruder_override = component && *component > 0 ? int(*component) - 1 : -1; + if (extruder_override >= 0) { + saw_override = true; + for (size_t idx = chunk_start; idx < chunk_end; ++idx) + path_segments[idx].extruder_override = extruder_override; + } + chunk_start = chunk_end; + } + + start = run_end; + } + } + if (!saw_segment || !saw_override) { if (emit_unchanged) pieces.push_back(PerimeterTextureRecolorEntityPiece{ -1, path.clone() }); @@ -1730,7 +1973,8 @@ static bool perimeter_texture_split_path_by_recolor_masks(const ExtrusionPath } if (std::isfinite(path.width) && path.width > 0.f) { - const double min_recolor_length_scaled = double(scale_(2.f * path.width)); + const double min_recolor_length_scaled = + double(scale_(std::max(2.f * path.width, context.min_recolor_run_length_mm))); perimeter_texture_expand_short_recolor_runs(path_segments, min_recolor_length_scaled); perimeter_texture_clear_short_recolor_runs(path_segments, min_recolor_length_scaled); } @@ -1777,18 +2021,22 @@ static bool perimeter_texture_split_path_by_recolor_masks(const ExtrusionPath static bool perimeter_texture_split_entity_by_recolor_masks(const ExtrusionEntity &entity, const PerimeterTexturePathRecolorContext &context, std::vector &pieces, - bool emit_unchanged); + bool emit_unchanged, + int path_depth_from_top); static bool perimeter_texture_split_paths_by_recolor_masks(const ExtrusionPaths &paths, const ExtrusionEntity &fallback_entity, const PerimeterTexturePathRecolorContext &context, std::vector &pieces, - bool emit_unchanged) + bool emit_unchanged, + int path_depth_from_top) { std::vector path_pieces; bool changed = false; - for (const ExtrusionPath &path : paths) - changed |= perimeter_texture_split_path_by_recolor_masks(path, context, path_pieces, true); + for (const ExtrusionPath &path : paths) { + const int effective_depth = path.inset_idx >= 0 ? path.inset_idx : path_depth_from_top; + changed |= perimeter_texture_split_path_by_recolor_masks(path, context, path_pieces, true, effective_depth); + } if (!changed) { perimeter_texture_delete_recolor_entity_pieces(path_pieces); @@ -1806,17 +2054,26 @@ static bool perimeter_texture_split_paths_by_recolor_masks(const ExtrusionPaths static bool perimeter_texture_split_collection_children_by_recolor_masks(const ExtrusionEntityCollection &collection, const PerimeterTexturePathRecolorContext &context, std::vector &pieces, - bool emit_unchanged) + bool emit_unchanged, + int path_depth_from_top) { std::vector child_pieces; bool changed = collection.texture_mapping_extruder_override >= 0; + int local_depth = path_depth_from_top; for (const ExtrusionEntity *child : collection.entities) { if (child == nullptr) continue; + const int child_depth = perimeter_texture_entity_depth_from_top(*child, local_depth); if (collection.texture_mapping_extruder_override >= 0) { child_pieces.push_back(PerimeterTextureRecolorEntityPiece{ collection.texture_mapping_extruder_override, child->clone() }); } else { - changed |= perimeter_texture_split_entity_by_recolor_masks(*child, context, child_pieces, true); + changed |= perimeter_texture_split_entity_by_recolor_masks(*child, context, child_pieces, true, child_depth); + } + if (child->inset_idx < 0 && perimeter_texture_entity_has_recolorable_path(*child)) { + if (is_external_perimeter(child->role())) + local_depth = 1; + else + ++local_depth; } } @@ -1840,16 +2097,17 @@ static bool perimeter_texture_split_collection_children_by_recolor_masks(const E static bool perimeter_texture_split_entity_by_recolor_masks(const ExtrusionEntity &entity, const PerimeterTexturePathRecolorContext &context, std::vector &pieces, - bool emit_unchanged) + bool emit_unchanged, + int path_depth_from_top) { if (const ExtrusionPath *path = dynamic_cast(&entity)) - return perimeter_texture_split_path_by_recolor_masks(*path, context, pieces, emit_unchanged); + return perimeter_texture_split_path_by_recolor_masks(*path, context, pieces, emit_unchanged, path_depth_from_top); if (const ExtrusionMultiPath *multipath = dynamic_cast(&entity)) - return perimeter_texture_split_paths_by_recolor_masks(multipath->paths, *multipath, context, pieces, emit_unchanged); + return perimeter_texture_split_paths_by_recolor_masks(multipath->paths, *multipath, context, pieces, emit_unchanged, path_depth_from_top); if (const ExtrusionLoop *loop = dynamic_cast(&entity)) - return perimeter_texture_split_paths_by_recolor_masks(loop->paths, *loop, context, pieces, emit_unchanged); + return perimeter_texture_split_paths_by_recolor_masks(loop->paths, *loop, context, pieces, emit_unchanged, path_depth_from_top); if (const ExtrusionEntityCollection *collection = dynamic_cast(&entity)) - return perimeter_texture_split_collection_children_by_recolor_masks(*collection, context, pieces, emit_unchanged); + return perimeter_texture_split_collection_children_by_recolor_masks(*collection, context, pieces, emit_unchanged, path_depth_from_top); if (emit_unchanged) pieces.push_back(PerimeterTextureRecolorEntityPiece{ -1, entity.clone() }); @@ -1892,7 +2150,8 @@ static void perimeter_texture_apply_top_visible_recolor_to_perimeters(const Laye const ExPolygons &path_mask, const TextureMappingZone &zone, unsigned int texture_zone_id, - float texture_external_width_mm) + float texture_external_width_mm, + float min_recolor_run_length_mm = 0.f) { if (perimeters.entities.empty() || path_mask.empty()) return; @@ -1905,6 +2164,12 @@ static void perimeter_texture_apply_top_visible_recolor_to_perimeters(const Laye PerimeterTexturePathRecolorContext context; context.path_mask = perimeter_texture_make_mask_index(&path_mask); context.sampler = &*sampler; + context.min_recolor_run_length_mm = std::max(0.f, min_recolor_run_length_mm); + context.contoning_stack_layers = sampler->contoning ? + perimeter_texture_available_contoning_shell_slots(perimeters, + std::max(1, layer_region.region().config().wall_loops.value), + sampler->contoning_stack_layers) : + sampler->contoning_stack_layers; if (context.path_mask.empty()) return; @@ -1920,9 +2185,9 @@ static void perimeter_texture_apply_top_visible_recolor_to_perimeters(const Laye recolored_entities.emplace_back(collection->clone()); continue; } - perimeter_texture_split_collection_children_by_recolor_masks(*collection, context, pieces, true); + perimeter_texture_split_collection_children_by_recolor_masks(*collection, context, pieces, true, 0); } else { - perimeter_texture_split_entity_by_recolor_masks(*entity, context, pieces, true); + perimeter_texture_split_entity_by_recolor_masks(*entity, context, pieces, true, 0); } const bool entity_changed = std::any_of(pieces.begin(), pieces.end(), [](const PerimeterTextureRecolorEntityPiece &piece) { @@ -2392,7 +2657,9 @@ void Layer::apply_perimeter_path_modulation_v2() layerm->slices, *zone, texture_zone_id, - std::nullopt, + zone->top_surface_contoning_perimeters_active() ? + std::optional(std::max(0.05f, float(layerm->flow(frExternalPerimeter).width()))) : + std::nullopt, nullptr, nullptr, nullptr); @@ -2475,26 +2742,58 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe const float texture_external_width_mm = std::max(0.05f, float(print_config.texture_mapping_outer_wall_gradient_max_line_width.value)); + const float normal_external_width_mm = std::max(0.05f, float(this->flow(frExternalPerimeter).width())); SurfaceCollection modulated_slices; const SurfaceCollection *perimeter_slices = &slices; ExPolygons top_visible_recolor_path_mask; std::vector top_visible_recolor_masks; PerimeterTextureTopVisibleRecolorThresholds top_visible_recolor_thresholds; + float contoning_min_recolor_run_length_mm = 0.f; std::optional reusable_modulation_context; unsigned int perimeter_texture_zone_id = 0; bool use_perimeter_path_modulation = false; bool use_legacy_perimeter_path_modulation = false; bool use_perimeter_path_modulation_v2 = false; + bool use_contoning_perimeter = false; + float active_external_width_mm = texture_external_width_mm; const TextureMappingZone *perimeter_path_zone = perimeter_path_modulation_zone_for_region(*this->layer()->object()->print(), region_config, perimeter_texture_zone_id); if (perimeter_path_zone != nullptr) { - if (perimeter_path_zone->recolor_top_visible_perimeter_sections) { + use_contoning_perimeter = perimeter_path_zone->top_surface_contoning_perimeters_active(); + active_external_width_mm = use_contoning_perimeter ? normal_external_width_mm : texture_external_width_mm; + if (use_contoning_perimeter) { + ExPolygons wall_band; + const float wall_depth_mm = + active_external_width_mm + + std::max(0, region_config.wall_loops.value - 1) * float(this->flow(frPerimeter).spacing()); + top_visible_recolor_path_mask = + perimeter_texture_top_visible_wall_band_mask(*this, + slices, + wall_depth_mm, + perimeter_path_zone->top_visible_perimeter_recolor_above_layers, + &wall_band); + if (perimeter_path_zone->top_surface_contoning_angle_threshold_deg >= + TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg - 1e-4f) + top_visible_recolor_path_mask = std::move(wall_band); + contoning_min_recolor_run_length_mm = + texture_mapping_contoning_min_feature_mm(*perimeter_path_zone, + print_config, + TextureMappingManager::effective_texture_component_ids( + *perimeter_path_zone, + print_config.filament_colour.values.size(), + print_config.filament_colour.values), + active_external_width_mm); + top_visible_recolor_thresholds = perimeter_texture_top_visible_recolor_thresholds( + int(TextureMappingZone::TopVisibleRecolorConservative)); + top_visible_recolor_thresholds.min_run_length_mm = contoning_min_recolor_run_length_mm; + top_visible_recolor_thresholds.min_visible_area_mm2 = contoning_min_recolor_run_length_mm * contoning_min_recolor_run_length_mm * 0.25f; + } else if (perimeter_path_zone->recolor_top_visible_perimeter_sections) { perimeter_texture_top_visible_recolor_data(*this, slices, *perimeter_path_zone, perimeter_texture_zone_id, - texture_external_width_mm, + active_external_width_mm, top_visible_recolor_masks, top_visible_recolor_path_mask, top_visible_recolor_thresholds, @@ -2598,7 +2897,8 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe top_visible_recolor_path_mask, *perimeter_path_zone, perimeter_texture_zone_id, - active_texture_external_width_mm.value_or(texture_external_width_mm)); + active_texture_external_width_mm.value_or(active_external_width_mm), + contoning_min_recolor_run_length_mm); }; SurfaceCollection fill_surfaces_before; @@ -2609,7 +2909,7 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe } const std::optional initial_texture_external_width_mm = - use_perimeter_path_modulation ? std::optional(texture_external_width_mm) : std::optional(); + use_perimeter_path_modulation ? std::optional(active_external_width_mm) : std::optional(); process_slices_with_top_visible_recolor(perimeter_slices, initial_texture_external_width_mm); if ((use_legacy_perimeter_path_modulation || use_perimeter_path_modulation_v2) && @@ -2621,18 +2921,18 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe *fill_no_overlap = fill_no_overlap_before; fill_surfaces_before = *fill_surfaces; fill_no_overlap_before = *fill_no_overlap; - process_slices(fallback_original_slices, std::optional(texture_external_width_mm)); + process_slices(fallback_original_slices, std::optional(active_external_width_mm)); const bool original_texture_has_extrusions = !this->perimeters.entities.empty() || !this->thin_fills.entities.empty(); const std::optional reduced_external_width_mm = perimeter_texture_min_external_width(this->perimeters); const bool has_reduced_external_width = reduced_external_width_mm && - *reduced_external_width_mm < texture_external_width_mm - float(EPSILON); + *reduced_external_width_mm < active_external_width_mm - float(EPSILON); if (has_reduced_external_width && perimeter_path_zone->recolor_small_perimeter_loops) { if (perimeter_texture_apply_recolor_small_perimeter_loops(*this, *perimeter_path_zone, perimeter_texture_zone_id, - texture_external_width_mm)) { + active_external_width_mm)) { set_perimeter_path_modulation_v2_fallback_slices(*fallback_original_slices, false); return; } @@ -2662,7 +2962,7 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe *fill_surfaces = fill_surfaces_before; *fill_no_overlap = fill_no_overlap_before; const std::optional fallback_texture_external_width_mm = - original_texture_has_extrusions ? std::optional(texture_external_width_mm) : std::optional(); + original_texture_has_extrusions ? std::optional(active_external_width_mm) : std::optional(); process_slices(fallback_original_slices, fallback_texture_external_width_mm); set_perimeter_path_modulation_v2_fallback_slices(*fallback_original_slices, false); } diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 26b2660b873..23099d3600c 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -521,8 +521,11 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // Append paths to collection. if (!paths.empty()) { + for (ExtrusionPath &path : paths) + path.inset_idx = int(extrusion->inset_idx); if (extrusion->is_closed) { ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); + extrusion_loop.inset_idx = int(extrusion->inset_idx); if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (pg_extrusion.is_contour || pg_extrusions.size() == 2)) extrusion_loop.make_counter_clockwise(); @@ -550,12 +553,14 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p assert(std::prev(it)->polyline.last_point() == it->polyline.first_point()); } ExtrusionMultiPath multi_path; + multi_path.inset_idx = int(extrusion->inset_idx); multi_path.paths.emplace_back(std::move(paths.front())); for (auto it_path = std::next(paths.begin()); it_path != paths.end(); ++it_path) { if (multi_path.paths.back().last_point() != it_path->first_point()) { extrusion_coll.append(ExtrusionMultiPath(std::move(multi_path))); multi_path = ExtrusionMultiPath(); + multi_path.inset_idx = int(extrusion->inset_idx); } multi_path.paths.emplace_back(std::move(*it_path)); } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f2956a27739..ab43f6afb7e 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -832,17 +833,28 @@ void PrintObject::infill() this->prepare_infill(); if (this->set_started(posInfill)) { - m_print->set_status(35, L("Generating infill toolpath")); + const size_t total_layers = m_layers.size(); + std::atomic completed_layers { 0 }; + auto set_infill_progress = [this, total_layers](size_t completed) { + if (total_layers == 0) + m_print->set_status(35, L("Generating infill toolpath")); + else + m_print->set_status(35, Slic3r::format(L("Generating infill toolpath (%1%/%2%)"), completed, total_layers)); + }; + set_infill_progress(0); const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first; const auto& support_fill_octree = this->m_adaptive_fill_octrees.second; + const std::function throw_if_canceled = [this]() { m_print->throw_if_canceled(); }; BOOST_LOG_TRIVIAL(debug) << "Filling layers in parallel - start"; tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), - [this, &adaptive_fill_octree = adaptive_fill_octree, &support_fill_octree = support_fill_octree](const tbb::blocked_range& range) { + [this, &adaptive_fill_octree = adaptive_fill_octree, &support_fill_octree = support_fill_octree, &throw_if_canceled, &completed_layers, &set_infill_progress](const tbb::blocked_range& range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); - m_layers[layer_idx]->make_fills(adaptive_fill_octree.get(), support_fill_octree.get(), this->m_lightning_generator.get()); + m_layers[layer_idx]->make_fills(adaptive_fill_octree.get(), support_fill_octree.get(), this->m_lightning_generator.get(), throw_if_canceled); + const size_t completed = completed_layers.fetch_add(1, std::memory_order_relaxed) + 1; + set_infill_progress(completed); } } ); diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index cc40f214c50..d05c43214ed 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -668,6 +668,8 @@ static int modulation_mode_from_name(const std::string &name) static std::string top_surface_image_printing_method_name(int method) { + if (method == int(TextureMappingZone::TopSurfaceImageContoning)) + return std::string("contoning"); if (method == int(TextureMappingZone::TopSurfaceImageSameLayer45Partition)) return std::string("same_layer_45_partition"); return std::string("same_angle_45_width"); @@ -675,6 +677,8 @@ static std::string top_surface_image_printing_method_name(int method) static int top_surface_image_printing_method_from_name(const std::string &name) { + if (name == "contoning") + return int(TextureMappingZone::TopSurfaceImageContoning); if (name == "same_layer_45_partition") return int(TextureMappingZone::TopSurfaceImageSameLayer45Partition); return int(TextureMappingZone::TopSurfaceImageSameAngle45Width); @@ -1044,6 +1048,11 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const std::abs(top_surface_image_max_line_width_mm - rhs.top_surface_image_max_line_width_mm) <= eps && top_surface_image_colored_top_layers == rhs.top_surface_image_colored_top_layers && top_surface_image_fixed_coloring_filaments == rhs.top_surface_image_fixed_coloring_filaments && + std::abs(top_surface_contoning_angle_threshold_deg - rhs.top_surface_contoning_angle_threshold_deg) <= eps && + top_surface_contoning_stack_layers == rhs.top_surface_contoning_stack_layers && + std::abs(top_surface_contoning_min_feature_mm - rhs.top_surface_contoning_min_feature_mm) <= eps && + top_surface_contoning_color_lower_surfaces == rhs.top_surface_contoning_color_lower_surfaces && + top_surface_contoning_only_color_surface_infill == rhs.top_surface_contoning_only_color_surface_infill && compact_offset_mode == rhs.compact_offset_mode && use_legacy_fixed_color_mode == rhs.use_legacy_fixed_color_mode && high_speed_image_texture_sampling == rhs.high_speed_image_texture_sampling && @@ -1401,6 +1410,22 @@ std::string TextureMappingManager::serialize_entries() TextureMappingZone::MinTopSurfaceImageColoredTopLayers, TextureMappingZone::MaxTopSurfaceImageColoredTopLayers); texture["top_surface_image_fixed_coloring_filaments"] = zone.top_surface_image_fixed_coloring_filaments; + texture["top_surface_contoning_angle_threshold_deg"] = + std::clamp(finite_or(zone.top_surface_contoning_angle_threshold_deg, + TextureMappingZone::DefaultTopSurfaceContoningAngleThresholdDeg), + TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg, + TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg); + texture["top_surface_contoning_stack_layers"] = + clamp_int(zone.top_surface_contoning_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + texture["top_surface_contoning_min_feature_mm"] = + std::clamp(finite_or(zone.top_surface_contoning_min_feature_mm, + TextureMappingZone::DefaultTopSurfaceContoningMinFeatureMm), + TextureMappingZone::MinTopSurfaceContoningMinFeatureMm, + TextureMappingZone::MaxTopSurfaceContoningMinFeatureMm); + texture["top_surface_contoning_color_lower_surfaces"] = zone.top_surface_contoning_color_lower_surfaces; + texture["top_surface_contoning_only_color_surface_infill"] = zone.top_surface_contoning_only_color_surface_infill; texture["compact_offset_mode"] = zone.compact_offset_mode; texture["use_legacy_fixed_color_mode"] = zone.use_legacy_fixed_color_mode; texture["high_speed_image_texture_sampling"] = true; @@ -1626,6 +1651,29 @@ void TextureMappingManager::load_entries(const std::string &serialized, zone.top_surface_image_fixed_coloring_filaments = texture.value("top_surface_image_fixed_coloring_filaments", TextureMappingZone::DefaultTopSurfaceImageFixedColoringFilaments); + zone.top_surface_contoning_angle_threshold_deg = + std::clamp(finite_or(texture.value("top_surface_contoning_angle_threshold_deg", + TextureMappingZone::DefaultTopSurfaceContoningAngleThresholdDeg), + TextureMappingZone::DefaultTopSurfaceContoningAngleThresholdDeg), + TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg, + TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg); + zone.top_surface_contoning_stack_layers = + clamp_int(texture.value("top_surface_contoning_stack_layers", + TextureMappingZone::DefaultTopSurfaceContoningStackLayers), + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + zone.top_surface_contoning_min_feature_mm = + std::clamp(finite_or(texture.value("top_surface_contoning_min_feature_mm", + TextureMappingZone::DefaultTopSurfaceContoningMinFeatureMm), + TextureMappingZone::DefaultTopSurfaceContoningMinFeatureMm), + TextureMappingZone::MinTopSurfaceContoningMinFeatureMm, + TextureMappingZone::MaxTopSurfaceContoningMinFeatureMm); + zone.top_surface_contoning_color_lower_surfaces = + texture.value("top_surface_contoning_color_lower_surfaces", + TextureMappingZone::DefaultTopSurfaceContoningColorLowerSurfaces); + zone.top_surface_contoning_only_color_surface_infill = + texture.value("top_surface_contoning_only_color_surface_infill", + TextureMappingZone::DefaultTopSurfaceContoningOnlyColorSurfaceInfill); zone.compact_offset_mode = texture.value("compact_offset_mode", TextureMappingZone::DefaultCompactOffsetMode); zone.use_legacy_fixed_color_mode = texture.value("use_legacy_fixed_color_mode", TextureMappingZone::DefaultUseLegacyFixedColorMode); diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index 027653a2d9c..a5ad8265d9c 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -68,7 +68,8 @@ struct TextureMappingZone enum TopSurfaceImagePrintingMethod : uint8_t { TopSurfaceImageSameAngle45Width = 0, - TopSurfaceImageSameLayer45Partition = 1 + TopSurfaceImageSameLayer45Partition = 1, + TopSurfaceImageContoning = 2 }; enum FilamentColorMode : uint8_t { @@ -155,6 +156,17 @@ struct TextureMappingZone static constexpr int MaxTopSurfaceImageColoredTopLayers = 20; static constexpr int DefaultTopSurfaceImageColoredTopLayers = 3; static constexpr bool DefaultTopSurfaceImageFixedColoringFilaments = true; + static constexpr float MinTopSurfaceContoningAngleThresholdDeg = 0.f; + static constexpr float MaxTopSurfaceContoningAngleThresholdDeg = 180.f; + static constexpr float DefaultTopSurfaceContoningAngleThresholdDeg = 20.f; + static constexpr int MinTopSurfaceContoningStackLayers = 1; + static constexpr int MaxTopSurfaceContoningStackLayers = 20; + static constexpr int DefaultTopSurfaceContoningStackLayers = 6; + static constexpr float MinTopSurfaceContoningMinFeatureMm = 0.f; + static constexpr float MaxTopSurfaceContoningMinFeatureMm = 20.f; + static constexpr float DefaultTopSurfaceContoningMinFeatureMm = 0.f; + static constexpr bool DefaultTopSurfaceContoningColorLowerSurfaces = true; + static constexpr bool DefaultTopSurfaceContoningOnlyColorSurfaceInfill = false; static constexpr bool DefaultCompactOffsetMode = true; static constexpr bool DefaultUseLegacyFixedColorMode = false; static constexpr bool DefaultHighSpeedImageTextureSampling = true; @@ -249,6 +261,11 @@ struct TextureMappingZone float top_surface_image_max_line_width_mm = DefaultTopSurfaceImageMaxLineWidthMm; int top_surface_image_colored_top_layers = DefaultTopSurfaceImageColoredTopLayers; bool top_surface_image_fixed_coloring_filaments = DefaultTopSurfaceImageFixedColoringFilaments; + float top_surface_contoning_angle_threshold_deg = DefaultTopSurfaceContoningAngleThresholdDeg; + int top_surface_contoning_stack_layers = DefaultTopSurfaceContoningStackLayers; + float top_surface_contoning_min_feature_mm = DefaultTopSurfaceContoningMinFeatureMm; + bool top_surface_contoning_color_lower_surfaces = DefaultTopSurfaceContoningColorLowerSurfaces; + bool top_surface_contoning_only_color_surface_infill = DefaultTopSurfaceContoningOnlyColorSurfaceInfill; bool compact_offset_mode = DefaultCompactOffsetMode; bool use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; bool high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; @@ -298,7 +315,20 @@ struct TextureMappingZone return top_surface_image_printing_enabled && is_image_texture() && (top_surface_image_printing_method == int(TopSurfaceImageSameAngle45Width) || - top_surface_image_printing_method == int(TopSurfaceImageSameLayer45Partition)); + top_surface_image_printing_method == int(TopSurfaceImageSameLayer45Partition) || + (top_surface_image_printing_method == int(TopSurfaceImageContoning) && + uses_perimeter_path_modulation_v2())); + } + bool top_surface_contoning_active() const + { + return top_surface_image_printing_enabled && + is_image_texture() && + top_surface_image_printing_method == int(TopSurfaceImageContoning) && + uses_perimeter_path_modulation_v2(); + } + bool top_surface_contoning_perimeters_active() const + { + return top_surface_contoning_active() && !top_surface_contoning_only_color_surface_infill; } void apply_default_modulation_mode() @@ -343,6 +373,11 @@ struct TextureMappingZone top_surface_image_max_line_width_mm = DefaultTopSurfaceImageMaxLineWidthMm; top_surface_image_colored_top_layers = DefaultTopSurfaceImageColoredTopLayers; top_surface_image_fixed_coloring_filaments = DefaultTopSurfaceImageFixedColoringFilaments; + top_surface_contoning_angle_threshold_deg = DefaultTopSurfaceContoningAngleThresholdDeg; + top_surface_contoning_stack_layers = DefaultTopSurfaceContoningStackLayers; + top_surface_contoning_min_feature_mm = DefaultTopSurfaceContoningMinFeatureMm; + top_surface_contoning_color_lower_surfaces = DefaultTopSurfaceContoningColorLowerSurfaces; + top_surface_contoning_only_color_surface_infill = DefaultTopSurfaceContoningOnlyColorSurfaceInfill; compact_offset_mode = DefaultCompactOffsetMode; use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; diff --git a/src/libslic3r/TextureMappingContoning.cpp b/src/libslic3r/TextureMappingContoning.cpp new file mode 100644 index 00000000000..b79506a15b0 --- /dev/null +++ b/src/libslic3r/TextureMappingContoning.cpp @@ -0,0 +1,269 @@ +// original author: sentientstardust + +#include "TextureMappingContoning.hpp" + +#include "Color.hpp" +#include "ColorSolver.hpp" +#include "PrintConfig.hpp" +#include "libslic3r.h" + +#include +#include +#include +#include + +namespace Slic3r { +namespace { + +float clamp01(float value) +{ + if (!std::isfinite(value)) + return 0.f; + return std::clamp(value, 0.f, 1.f); +} + +float filament_luminance(const PrintConfig &config, unsigned int component_id) +{ + ColorRGB color; + if (component_id == 0 || component_id > config.filament_colour.values.size() || + !decode_color(config.filament_colour.get_at(size_t(component_id - 1)), color)) + return std::numeric_limits::max(); + return 0.2126f * color.r() + 0.7152f * color.g() + 0.0722f * color.b(); +} + +float perceptual_error(const std::array &lhs, const std::array &rhs) +{ + const float dl = lhs[0] - rhs[0]; + const float da = lhs[1] - rhs[1]; + const float db = lhs[2] - rhs[2]; + return dl * dl + 4.f * da * da + 4.f * db * db; +} + +void enumerate_counts(size_t component_idx, + int remaining, + std::vector &counts, + std::vector> &out) +{ + if (component_idx + 1 == counts.size()) { + counts[component_idx] = remaining; + out.emplace_back(counts); + return; + } + for (int count = 0; count <= remaining; ++count) { + counts[component_idx] = count; + enumerate_counts(component_idx + 1, remaining - count, counts, out); + } +} + +} + +std::optional> texture_mapping_contoning_component_colors( + const PrintConfig &config, + const std::vector &component_ids, + std::vector> &out) +{ + out.clear(); + out.reserve(component_ids.size()); + for (const unsigned int id : component_ids) { + if (id == 0 || id > config.filament_colour.values.size()) + return std::nullopt; + ColorRGB color; + if (!decode_color(config.filament_colour.get_at(size_t(id - 1)), color)) + return std::nullopt; + out.push_back({ color.r(), color.g(), color.b() }); + } + if (out.empty()) + return std::nullopt; + return out.front(); +} + +std::vector texture_mapping_contoning_components_bottom_to_top( + const TextureMappingZone &zone, + const PrintConfig &config, + std::vector component_ids) +{ + component_ids.erase(std::remove_if(component_ids.begin(), component_ids.end(), [](unsigned int id) { + return id == 0; + }), component_ids.end()); + component_ids.erase(std::unique(component_ids.begin(), component_ids.end()), component_ids.end()); + if (component_ids.empty()) + return component_ids; + + bool has_complete_td = true; + for (unsigned int component_id : component_ids) { + const size_t idx = size_t(component_id - 1); + const float td = idx < zone.filament_transmission_distances_mm.size() ? + zone.filament_transmission_distances_mm[idx] : + 0.f; + if (!std::isfinite(td) || td <= 0.f) { + has_complete_td = false; + break; + } + } + if (has_complete_td) { + std::stable_sort(component_ids.begin(), component_ids.end(), [&zone](unsigned int lhs, unsigned int rhs) { + return zone.filament_transmission_distances_mm[size_t(lhs - 1)] < + zone.filament_transmission_distances_mm[size_t(rhs - 1)]; + }); + return component_ids; + } + + std::stable_sort(component_ids.begin(), component_ids.end(), [&config](unsigned int lhs, unsigned int rhs) { + return filament_luminance(config, lhs) < filament_luminance(config, rhs); + }); + return component_ids; +} + +float texture_mapping_contoning_min_feature_mm(const TextureMappingZone &zone, + const PrintConfig &config, + const std::vector &component_ids, + float external_width_mm) +{ + const float configured = + std::clamp(zone.top_surface_contoning_min_feature_mm, + TextureMappingZone::MinTopSurfaceContoningMinFeatureMm, + TextureMappingZone::MaxTopSurfaceContoningMinFeatureMm); + if (configured > 0.f) + return configured; + + float nozzle = 0.4f; + for (const unsigned int id : component_ids) { + if (id >= 1 && size_t(id - 1) < config.nozzle_diameter.values.size()) + nozzle = std::max(nozzle, float(config.nozzle_diameter.get_at(size_t(id - 1)))); + } + if (!config.nozzle_diameter.values.empty()) + nozzle = std::max(nozzle, float(config.nozzle_diameter.values.front())); + + const float width = std::isfinite(external_width_mm) && external_width_mm > 0.f ? external_width_mm : nozzle; + return std::max({ 2.0f, 4.f * nozzle, 3.f * width }); +} + +bool texture_mapping_contoning_normal_eligible(float normal_z, float threshold_deg) +{ + if (!std::isfinite(threshold_deg) || threshold_deg >= TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg - 1e-4f) + return true; + if (!std::isfinite(normal_z)) + return true; + const float clamped_z = std::clamp(normal_z, -1.f, 1.f); + const float angle = float(std::acos(clamped_z) * 180.0 / PI); + return angle <= std::clamp(threshold_deg, + TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg, + TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg); +} + +TextureMappingContoningSolver::TextureMappingContoningSolver(const TextureMappingZone &zone, + const PrintConfig &config, + std::vector component_ids) +{ + component_ids.erase(std::remove_if(component_ids.begin(), component_ids.end(), [&config](unsigned int id) { + return id == 0 || id > config.filament_colour.values.size(); + }), component_ids.end()); + component_ids.erase(std::unique(component_ids.begin(), component_ids.end()), component_ids.end()); + m_component_ids = component_ids; + if (!texture_mapping_contoning_component_colors(config, m_component_ids, m_component_colors)) + m_component_ids.clear(); + if (m_component_ids.empty()) + return; + + m_component_luminance.reserve(m_component_ids.size()); + for (const unsigned int id : m_component_ids) + m_component_luminance.emplace_back(filament_luminance(config, id)); + m_components_bottom_to_top = texture_mapping_contoning_components_bottom_to_top(zone, config, m_component_ids); +} + +const std::vector& +TextureMappingContoningSolver::candidates_for_depth(int stack_layers) const +{ + const int depth = std::clamp(stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + auto found = m_candidates_by_depth.find(depth); + if (found != m_candidates_by_depth.end()) + return found->second; + + std::vector candidates; + if (!valid()) + return m_candidates_by_depth.emplace(depth, std::move(candidates)).first->second; + + std::vector> counts_list; + std::vector counts(m_component_ids.size(), 0); + enumerate_counts(0, depth, counts, counts_list); + candidates.reserve(counts_list.size()); + for (std::vector &candidate_counts : counts_list) { + std::vector weights(candidate_counts.size(), 0.f); + for (size_t idx = 0; idx < candidate_counts.size(); ++idx) + weights[idx] = float(candidate_counts[idx]) / float(std::max(1, depth)); + + Candidate candidate; + candidate.counts = std::move(candidate_counts); + candidate.rgb = mix_color_solver_components(m_component_colors, weights, ColorSolverMixModel::PigmentPainter); + candidate.oklab = color_solver_oklab_from_srgb(candidate.rgb); + for (size_t idx = 0; idx < candidate.counts.size() && idx < m_component_luminance.size(); ++idx) + candidate.dark_score += float(candidate.counts[idx]) * (1.f - clamp01(m_component_luminance[idx])); + candidates.emplace_back(std::move(candidate)); + } + + return m_candidates_by_depth.emplace(depth, std::move(candidates)).first->second; +} + +TextureMappingContoningStack TextureMappingContoningSolver::solve(const std::array &target_rgb, int stack_layers) const +{ + TextureMappingContoningStack out; + if (!valid()) + return out; + + const int depth = std::clamp(stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers); + const std::vector &candidates = candidates_for_depth(depth); + if (candidates.empty()) + return out; + + const std::array target_oklab = color_solver_oklab_from_srgb(target_rgb); + size_t best_idx = size_t(-1); + float best_error = std::numeric_limits::max(); + float best_dark_score = -std::numeric_limits::max(); + for (size_t idx = 0; idx < candidates.size(); ++idx) { + const Candidate &candidate = candidates[idx]; + const float error = perceptual_error(candidate.oklab, target_oklab); + const bool near_tie = std::isfinite(best_error) && error <= best_error * 1.03f + 1e-6f; + if (error < best_error || (near_tie && candidate.dark_score > best_dark_score)) { + best_idx = idx; + best_error = error; + best_dark_score = candidate.dark_score; + } + } + if (best_idx >= candidates.size()) + return out; + + const Candidate &best = candidates[best_idx]; + out.bottom_to_top.reserve(size_t(depth)); + for (const unsigned int ordered_id : m_components_bottom_to_top) { + const auto component_it = std::find(m_component_ids.begin(), m_component_ids.end(), ordered_id); + if (component_it == m_component_ids.end()) + continue; + const size_t component_idx = size_t(component_it - m_component_ids.begin()); + const int count = component_idx < best.counts.size() ? best.counts[component_idx] : 0; + for (int i = 0; i < count; ++i) + out.bottom_to_top.emplace_back(ordered_id); + } + while (int(out.bottom_to_top.size()) < depth) + out.bottom_to_top.emplace_back(m_components_bottom_to_top.empty() ? m_component_ids.front() : m_components_bottom_to_top.back()); + if (int(out.bottom_to_top.size()) > depth) + out.bottom_to_top.resize(size_t(depth)); + return out; +} + +unsigned int TextureMappingContoningSolver::component_for_depth(const std::array &target_rgb, + int stack_layers, + int depth_from_top) const +{ + TextureMappingContoningStack stack = solve(target_rgb, stack_layers); + if (stack.bottom_to_top.empty()) + return 0; + const int depth = int(stack.bottom_to_top.size()); + const int idx = std::clamp(depth - 1 - depth_from_top, 0, depth - 1); + return stack.bottom_to_top[size_t(idx)]; +} + +} diff --git a/src/libslic3r/TextureMappingContoning.hpp b/src/libslic3r/TextureMappingContoning.hpp new file mode 100644 index 00000000000..6d8307932fd --- /dev/null +++ b/src/libslic3r/TextureMappingContoning.hpp @@ -0,0 +1,67 @@ +// original author: sentientstardust + +#ifndef slic3r_TextureMappingContoning_hpp_ +#define slic3r_TextureMappingContoning_hpp_ + +#include "TextureMapping.hpp" + +#include +#include +#include +#include + +namespace Slic3r { + +class PrintConfig; + +struct TextureMappingContoningStack { + std::vector bottom_to_top; +}; + +class TextureMappingContoningSolver +{ +public: + TextureMappingContoningSolver() = default; + TextureMappingContoningSolver(const TextureMappingZone &zone, + const PrintConfig &config, + std::vector component_ids); + + bool valid() const { return !m_component_ids.empty() && m_component_ids.size() == m_component_colors.size(); } + const std::vector& component_ids() const { return m_component_ids; } + const std::vector& components_bottom_to_top() const { return m_components_bottom_to_top; } + + TextureMappingContoningStack solve(const std::array &target_rgb, int stack_layers) const; + unsigned int component_for_depth(const std::array &target_rgb, int stack_layers, int depth_from_top) const; + +private: + struct Candidate { + std::array rgb { { 0.f, 0.f, 0.f } }; + std::array oklab { { 0.f, 0.f, 0.f } }; + std::vector counts; + float dark_score { 0.f }; + }; + + const std::vector& candidates_for_depth(int stack_layers) const; + + std::vector m_component_ids; + std::vector m_components_bottom_to_top; + std::vector> m_component_colors; + std::vector m_component_luminance; + mutable std::map> m_candidates_by_depth; +}; + +std::vector texture_mapping_contoning_components_bottom_to_top(const TextureMappingZone &zone, + const PrintConfig &config, + std::vector component_ids); +float texture_mapping_contoning_min_feature_mm(const TextureMappingZone &zone, + const PrintConfig &config, + const std::vector &component_ids, + float external_width_mm); +bool texture_mapping_contoning_normal_eligible(float normal_z, float threshold_deg); +std::optional> texture_mapping_contoning_component_colors(const PrintConfig &config, + const std::vector &component_ids, + std::vector> &out); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/TextureMappingOffset.cpp b/src/libslic3r/TextureMappingOffset.cpp index aecfe858a53..947019694ea 100644 --- a/src/libslic3r/TextureMappingOffset.cpp +++ b/src/libslic3r/TextureMappingOffset.cpp @@ -33,6 +33,7 @@ struct TextureSampleData { std::array rgba { { 0.f, 0.f, 0.f, 1.f } }; std::vector raw_component_weights; bool raw_component_weights_from_texture { false }; + float normal_z { std::numeric_limits::quiet_NaN() }; }; struct WeightedTextureSample { @@ -41,6 +42,7 @@ struct WeightedTextureSample { std::array rgba { { 0.f, 0.f, 0.f, 1.f } }; std::vector raw_component_weights; bool raw_component_weights_from_texture { false }; + float normal_z { std::numeric_limits::quiet_NaN() }; float weight { 0.f }; }; @@ -1412,7 +1414,8 @@ bool accumulate_layer_plane_triangle_samples(const Vec3d &p0, sample_data.rgba, sample_weight, std::move(sample_data.raw_component_weights), - sample_data.raw_component_weights_from_texture); + sample_data.raw_component_weights_from_texture, + sample_data.normal_z); } return true; @@ -1550,7 +1553,8 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( const std::array &rgba, float sample_weight, std::vector raw_component_weights = {}, - bool raw_component_weights_from_texture = false) { + bool raw_component_weights_from_texture = false, + float normal_z = std::numeric_limits::quiet_NaN()) { if (!std::isfinite(x_mm) || !std::isfinite(y_mm) || sample_weight <= EPSILON) return; if (!std::isfinite(sample_weight) || @@ -1561,16 +1565,18 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( return; if (raw_component_weights.size() != component_count) raw_component_weights_from_texture = false; - samples.push_back({ x_mm, y_mm, rgba, std::move(raw_component_weights), raw_component_weights_from_texture, sample_weight }); + samples.push_back({ x_mm, y_mm, rgba, std::move(raw_component_weights), raw_component_weights_from_texture, normal_z, sample_weight }); }; auto accumulate_constant_surface_triangle_samples = [&](const Vec3d &p0, const Vec3d &p1, const Vec3d &p2, const std::array &rgba) { + const Vec3d normal = (p1 - p0).cross(p2 - p0).normalized(); + const float normal_z = normal.allFinite() ? float(normal.z()) : std::numeric_limits::quiet_NaN(); if (accumulate_layer_plane_triangle_samples(p0, p1, p2, layer_z_mm, safe_layer_z_falloff_mm, high_resolution_texture_sampling, physical_sample_pitch_mm, - [&rgba](const Vec3f &) { return TextureSampleData{ rgba, {}, false }; }, accumulate_sample)) + [&rgba, normal_z](const Vec3f &) { return TextureSampleData{ rgba, {}, false, normal_z }; }, accumulate_sample)) return; const float max_world_edge_mm = std::max({ float((p1 - p0).norm()), float((p2 - p1).norm()), float((p0 - p2).norm()) }); @@ -1601,7 +1607,7 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( const float z_weight = std::exp(-0.5f * z_norm * z_norm); if (!std::isfinite(z_weight) || z_weight <= EPSILON) continue; - accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, area_weight * z_weight); + accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, area_weight * z_weight, {}, false, normal_z); } } }; @@ -1719,6 +1725,8 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( const Vec3d p2 = volume_trafo * its.vertices[size_t(tri[2])].cast(); if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) continue; + const Vec3d normal = (p1 - p0).cross(p2 - p0).normalized(); + const float normal_z = normal.allFinite() ? float(normal.z()) : std::numeric_limits::quiet_NaN(); const size_t uv_off = tri_idx * 6; const std::array uvs = unwrap_triangle_uvs( @@ -1728,9 +1736,11 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( if (accumulate_layer_plane_triangle_samples(p0, p1, p2, layer_z_mm, safe_layer_z_falloff_mm, high_resolution_texture_sampling, physical_sample_pitch_mm, - [&uvs, &sample_data_for_uv](const Vec3f &barycentric) { + [&uvs, &sample_data_for_uv, normal_z](const Vec3f &barycentric) { const Vec2f uv = uvs[0] * barycentric.x() + uvs[1] * barycentric.y() + uvs[2] * barycentric.z(); - return sample_data_for_uv(uv); + TextureSampleData sample_data = sample_data_for_uv(uv); + sample_data.normal_z = normal_z; + return sample_data; }, accumulate_sample)) continue; @@ -1774,7 +1784,8 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( sample_data.rgba, area_weight * z_weight, std::move(sample_data.raw_component_weights), - sample_data.raw_component_weights_from_texture); + sample_data.raw_component_weights_from_texture, + normal_z); } } } @@ -1960,6 +1971,7 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( weight_field.sample_r.resize(sample_count); weight_field.sample_g.resize(sample_count); weight_field.sample_b.resize(sample_count); + weight_field.sample_normal_z.resize(sample_count); weight_field.sample_component_weights.assign(sample_count * component_count, 0.f); weight_field.raw_component_weights_from_texture = false; weight_field.binary_dithered = !binary_dither_masks.empty(); @@ -1979,6 +1991,7 @@ TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( weight_field.sample_r[sample_idx] = target_rgb[0]; weight_field.sample_g[sample_idx] = target_rgb[1]; weight_field.sample_b[sample_idx] = target_rgb[2]; + weight_field.sample_normal_z[sample_idx] = sample.normal_z; std::vector desired(component_count, 0.f); const bool has_binary_dither = sample_idx < binary_dither_masks.size() && binary_dither_masks[sample_idx] != 0; @@ -2735,6 +2748,81 @@ std::optional> sample_weight_field_rgb(const TextureMapping return std::nullopt; } +std::optional sample_weight_field_normal_z(const TextureMappingOffsetWeightField &weight_field, + float x_mm, + float y_mm, + bool high_resolution_texture_sampling) +{ + if (weight_field.empty() || + weight_field.sample_normal_z.size() != weight_field.sample_x_mm.size() || + !std::isfinite(x_mm) || + !std::isfinite(y_mm)) + return std::nullopt; + + const float gx = (x_mm - weight_field.min_x_mm) / std::max(weight_field.bucket_width_mm, 1e-6f); + const float gy = (y_mm - weight_field.min_y_mm) / std::max(weight_field.bucket_height_mm, 1e-6f); + const int cx = std::clamp(int(std::floor(gx)), 0, weight_field.bucket_width - 1); + const int cy = std::clamp(int(std::floor(gy)), 0, weight_field.bucket_height - 1); + const float max_radius_mm = + high_resolution_texture_sampling ? + std::max(0.18f, std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * 2.5f) : + std::max(0.32f, std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * 3.0f); + const int max_ring = std::clamp(int(std::ceil(max_radius_mm / std::max(std::min(weight_field.bucket_width_mm, + weight_field.bucket_height_mm), + 1e-3f))), + 1, + std::max(weight_field.bucket_width, weight_field.bucket_height)); + double weighted_normal_z = 0.0; + double total_weight = 0.0; + size_t nearest_sample_idx = size_t(-1); + float nearest_d2 = std::numeric_limits::max(); + + for (int ring = 0; ring <= max_ring; ++ring) { + const int min_x = std::max(0, cx - ring); + const int max_x = std::min(weight_field.bucket_width - 1, cx + ring); + const int min_y = std::max(0, cy - ring); + const int max_y = std::min(weight_field.bucket_height - 1, cy + ring); + for (int by = min_y; by <= max_y; ++by) { + for (int bx = min_x; bx <= max_x; ++bx) { + const size_t bucket_idx = size_t(by) * size_t(weight_field.bucket_width) + size_t(bx); + if (bucket_idx >= weight_field.buckets.size()) + continue; + for (const uint32_t sample_idx_u32 : weight_field.buckets[bucket_idx]) { + const size_t sample_idx = size_t(sample_idx_u32); + if (sample_idx >= weight_field.sample_x_mm.size() || sample_idx >= weight_field.sample_normal_z.size()) + continue; + const float normal_z = weight_field.sample_normal_z[sample_idx]; + if (!std::isfinite(normal_z)) + continue; + const float dx = x_mm - weight_field.sample_x_mm[sample_idx]; + const float dy = y_mm - weight_field.sample_y_mm[sample_idx]; + const float d2 = dx * dx + dy * dy; + if (d2 < nearest_d2) { + nearest_d2 = d2; + nearest_sample_idx = sample_idx; + } + if (d2 > max_radius_mm * max_radius_mm) + continue; + const float kernel = std::exp(-0.5f * d2 / std::max(max_radius_mm * max_radius_mm * 0.16f, 1e-6f)); + const float sample_w = weight_field.sample_weight[sample_idx] * kernel; + if (!std::isfinite(sample_w) || sample_w <= EPSILON) + continue; + weighted_normal_z += double(normal_z) * double(sample_w); + total_weight += double(sample_w); + } + } + } + if (total_weight > EPSILON) + break; + } + + if (total_weight > EPSILON) + return std::clamp(float(weighted_normal_z / total_weight), -1.f, 1.f); + if (nearest_sample_idx != size_t(-1) && nearest_sample_idx < weight_field.sample_normal_z.size()) + return std::clamp(weight_field.sample_normal_z[nearest_sample_idx], -1.f, 1.f); + return std::nullopt; +} + std::vector decode_texture_mapping_offset_component_ids(const TextureMappingZone &zone, size_t num_physical) { if (zone.is_linear_gradient()) @@ -2817,7 +2905,8 @@ std::optional build_texture_mapping_offset_context_ std::optional layer_height_mm_override, std::optional plate_origin_mm_override, std::optional min_outer_width_mm_override, - std::optional> image_background_rgba_override) + std::optional> image_background_rgba_override, + std::optional sample_z_mm_override) { const Print *print = print_object.print(); if (print == nullptr) @@ -2958,6 +3047,9 @@ std::optional build_texture_mapping_offset_context_ const float layer_height_mm = layer_height_mm_override ? std::max(0.01f, *layer_height_mm_override) : std::max(0.01f, float(layer.height)); + const float sample_z_mm = sample_z_mm_override && std::isfinite(*sample_z_mm_override) ? + *sample_z_mm_override : + float(layer.print_z); const float min_width_for_positive_spacing_mm = layer_height_mm * float(1. - 0.25 * PI) + 1e-4f; const float safe_min_gradient_width_mm = std::clamp( std::max(config_min_gradient_width_mm, min_width_for_positive_spacing_mm), @@ -3021,7 +3113,7 @@ std::optional build_texture_mapping_offset_context_ component_minimum_offset_factors, texture_contrast_pct, texture_tone_gamma, - float(layer.print_z), + sample_z_mm, layer_sample_falloff_mm, high_resolution_texture_sampling, zone.high_speed_image_texture_sampling, diff --git a/src/libslic3r/TextureMappingOffset.hpp b/src/libslic3r/TextureMappingOffset.hpp index 0e9a2440273..b4fe79ea614 100644 --- a/src/libslic3r/TextureMappingOffset.hpp +++ b/src/libslic3r/TextureMappingOffset.hpp @@ -33,6 +33,7 @@ struct TextureMappingOffsetWeightField { std::vector sample_r; std::vector sample_g; std::vector sample_b; + std::vector sample_normal_z; std::vector sample_component_weights; std::vector> buckets; std::vector fallback_weights; @@ -105,7 +106,8 @@ std::optional build_texture_mapping_offset_context_ std::optional layer_height_mm_override = std::nullopt, std::optional plate_origin_mm_override = std::nullopt, std::optional min_outer_width_mm_override = std::nullopt, - std::optional> image_background_rgba_override = std::nullopt); + std::optional> image_background_rgba_override = std::nullopt, + std::optional sample_z_mm_override = std::nullopt); std::vector sample_weight_field_components(const TextureMappingOffsetWeightField &weight_field, float x_mm, @@ -117,6 +119,11 @@ std::optional> sample_weight_field_rgb(const TextureMapping float y_mm, bool high_resolution_texture_sampling); +std::optional sample_weight_field_normal_z(const TextureMappingOffsetWeightField &weight_field, + float x_mm, + float y_mm, + bool high_resolution_texture_sampling); + float texture_mapping_offset_surface_inset_mm(const TextureMappingOffsetContext &context, const Point &point, double inward_x, diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 909aaf3ad05..8f0a10ec928 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1873,6 +1873,11 @@ public: float top_surface_image_max_line_width_mm, int top_surface_image_colored_top_layers, bool top_surface_image_fixed_coloring_filaments, + float top_surface_contoning_angle_threshold_deg, + int top_surface_contoning_stack_layers, + float top_surface_contoning_min_feature_mm, + bool top_surface_contoning_color_lower_surfaces, + bool top_surface_contoning_only_color_surface_infill, const TextureMappingManager &texture_mapping_zones, const TextureMappingGlobalSettings &global_settings, const TextureMappingPrimeTowerImage &prime_tower_image, @@ -2338,11 +2343,12 @@ public: wxArrayString top_surface_method_choices; top_surface_method_choices.Add(_L("45 degree width modulation")); top_surface_method_choices.Add(_L("Same-layer 45 partition")); + top_surface_method_choices.Add(_L("Contoning")); m_top_surface_image_method_choice = new wxChoice(top_surface_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, top_surface_method_choices); m_top_surface_image_method_choice->SetSelection(std::clamp(top_surface_image_printing_method, int(TextureMappingZone::TopSurfaceImageSameAngle45Width), - int(TextureMappingZone::TopSurfaceImageSameLayer45Partition))); + int(TextureMappingZone::TopSurfaceImageContoning))); top_surface_method_row->Add(m_top_surface_image_method_choice, 1, wxALIGN_CENTER_VERTICAL); top_surface_box->Add(top_surface_method_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); auto *top_surface_width_row = new wxBoxSizer(wxHORIZONTAL); @@ -2410,10 +2416,86 @@ public: 0, wxALIGN_CENTER_VERTICAL); top_surface_box->Add(m_top_surface_image_colored_top_layers_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_top_surface_contoning_panel = new wxPanel(top_surface_page, wxID_ANY); + auto *top_surface_contoning_root = new wxBoxSizer(wxVERTICAL); + m_top_surface_contoning_panel->SetSizer(top_surface_contoning_root); + auto *contoning_angle_row = new wxBoxSizer(wxHORIZONTAL); + contoning_angle_row->Add(new wxStaticText(m_top_surface_contoning_panel, wxID_ANY, _L("Surface angle threshold")), + 0, + wxALIGN_CENTER_VERTICAL | wxRIGHT, + gap); + m_top_surface_contoning_angle_threshold_spin = + new wxSpinCtrlDouble(m_top_surface_contoning_panel, + wxID_ANY, + wxEmptyString, + wxDefaultPosition, + wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, + double(TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg), + double(TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg), + std::clamp(double(top_surface_contoning_angle_threshold_deg), + double(TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg), + double(TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg)), + 1.0); + m_top_surface_contoning_angle_threshold_spin->SetDigits(0); + contoning_angle_row->Add(m_top_surface_contoning_angle_threshold_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + contoning_angle_row->Add(new wxStaticText(m_top_surface_contoning_panel, wxID_ANY, _L("deg")), 0, wxALIGN_CENTER_VERTICAL); + top_surface_contoning_root->Add(contoning_angle_row, 0, wxEXPAND | wxBOTTOM, gap); + auto *contoning_layers_row = new wxBoxSizer(wxHORIZONTAL); + contoning_layers_row->Add(new wxStaticText(m_top_surface_contoning_panel, wxID_ANY, _L("Stack layers")), + 0, + wxALIGN_CENTER_VERTICAL | wxRIGHT, + gap); + m_top_surface_contoning_stack_layers_spin = + new wxSpinCtrl(m_top_surface_contoning_panel, + wxID_ANY, + wxEmptyString, + wxDefaultPosition, + wxSize(FromDIP(70), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers, + std::clamp(top_surface_contoning_stack_layers, + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers)); + contoning_layers_row->Add(m_top_surface_contoning_stack_layers_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + contoning_layers_row->Add(new wxStaticText(m_top_surface_contoning_panel, wxID_ANY, _L("layers")), 0, wxALIGN_CENTER_VERTICAL); + top_surface_contoning_root->Add(contoning_layers_row, 0, wxEXPAND | wxBOTTOM, gap); + auto *contoning_feature_row = new wxBoxSizer(wxHORIZONTAL); + contoning_feature_row->Add(new wxStaticText(m_top_surface_contoning_panel, wxID_ANY, _L("Minimum feature")), + 0, + wxALIGN_CENTER_VERTICAL | wxRIGHT, + gap); + m_top_surface_contoning_min_feature_spin = + new wxSpinCtrlDouble(m_top_surface_contoning_panel, + wxID_ANY, + wxEmptyString, + wxDefaultPosition, + wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, + double(TextureMappingZone::MinTopSurfaceContoningMinFeatureMm), + double(TextureMappingZone::MaxTopSurfaceContoningMinFeatureMm), + std::clamp(double(top_surface_contoning_min_feature_mm), + double(TextureMappingZone::MinTopSurfaceContoningMinFeatureMm), + double(TextureMappingZone::MaxTopSurfaceContoningMinFeatureMm)), + 0.1); + m_top_surface_contoning_min_feature_spin->SetDigits(1); + contoning_feature_row->Add(m_top_surface_contoning_min_feature_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + contoning_feature_row->Add(new wxStaticText(m_top_surface_contoning_panel, wxID_ANY, _L("mm")), 0, wxALIGN_CENTER_VERTICAL); + top_surface_contoning_root->Add(contoning_feature_row, 0, wxEXPAND); + top_surface_box->Add(m_top_surface_contoning_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); m_top_surface_image_fixed_coloring_filaments_checkbox = new wxCheckBox(top_surface_page, wxID_ANY, _L("Fixed top-surface coloring filaments")); m_top_surface_image_fixed_coloring_filaments_checkbox->SetValue(top_surface_image_fixed_coloring_filaments); - top_surface_box->Add(m_top_surface_image_fixed_coloring_filaments_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, gap); + top_surface_box->Add(m_top_surface_image_fixed_coloring_filaments_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_top_surface_contoning_color_lower_surfaces_checkbox = + new wxCheckBox(top_surface_page, wxID_ANY, _L("Also color lower surfaces")); + m_top_surface_contoning_color_lower_surfaces_checkbox->SetValue(top_surface_contoning_color_lower_surfaces); + top_surface_box->Add(m_top_surface_contoning_color_lower_surfaces_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_top_surface_contoning_only_color_surface_infill_checkbox = + new wxCheckBox(top_surface_page, wxID_ANY, _L("Only color surface infill")); + m_top_surface_contoning_only_color_surface_infill_checkbox->SetValue(top_surface_contoning_only_color_surface_infill); + top_surface_box->Add(m_top_surface_contoning_only_color_surface_infill_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, gap); m_top_surface_image_printing_enabled_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { update_top_surface_image_options_visibility(false); }); @@ -2682,7 +2764,7 @@ public: return m_top_surface_image_method_choice ? std::clamp(m_top_surface_image_method_choice->GetSelection(), int(TextureMappingZone::TopSurfaceImageSameAngle45Width), - int(TextureMappingZone::TopSurfaceImageSameLayer45Partition)) : + int(TextureMappingZone::TopSurfaceImageContoning)) : TextureMappingZone::DefaultTopSurfaceImagePrintingMethod; } float top_surface_image_min_line_width_mm() const @@ -2721,6 +2803,40 @@ public: return m_top_surface_image_fixed_coloring_filaments_checkbox == nullptr || m_top_surface_image_fixed_coloring_filaments_checkbox->GetValue(); } + float top_surface_contoning_angle_threshold_deg() const + { + return float(std::clamp(m_top_surface_contoning_angle_threshold_spin != nullptr ? + m_top_surface_contoning_angle_threshold_spin->GetValue() : + double(TextureMappingZone::DefaultTopSurfaceContoningAngleThresholdDeg), + double(TextureMappingZone::MinTopSurfaceContoningAngleThresholdDeg), + double(TextureMappingZone::MaxTopSurfaceContoningAngleThresholdDeg))); + } + int top_surface_contoning_stack_layers() const + { + return m_top_surface_contoning_stack_layers_spin ? + std::clamp(m_top_surface_contoning_stack_layers_spin->GetValue(), + TextureMappingZone::MinTopSurfaceContoningStackLayers, + TextureMappingZone::MaxTopSurfaceContoningStackLayers) : + TextureMappingZone::DefaultTopSurfaceContoningStackLayers; + } + float top_surface_contoning_min_feature_mm() const + { + return float(std::clamp(m_top_surface_contoning_min_feature_spin != nullptr ? + m_top_surface_contoning_min_feature_spin->GetValue() : + double(TextureMappingZone::DefaultTopSurfaceContoningMinFeatureMm), + double(TextureMappingZone::MinTopSurfaceContoningMinFeatureMm), + double(TextureMappingZone::MaxTopSurfaceContoningMinFeatureMm))); + } + bool top_surface_contoning_color_lower_surfaces() const + { + return m_top_surface_contoning_color_lower_surfaces_checkbox == nullptr || + m_top_surface_contoning_color_lower_surfaces_checkbox->GetValue(); + } + bool top_surface_contoning_only_color_surface_infill() const + { + return m_top_surface_contoning_only_color_surface_infill_checkbox != nullptr && + m_top_surface_contoning_only_color_surface_infill_checkbox->GetValue(); + } bool minimum_visibility_offset_enabled() const { return m_minimum_visibility_offset_checkbox && m_minimum_visibility_offset_checkbox->GetValue(); @@ -3106,6 +3222,15 @@ private: m_top_surface_image_min_line_width_spin->Enable(enabled); if (m_top_surface_image_max_line_width_spin != nullptr) m_top_surface_image_max_line_width_spin->Enable(enabled); + const bool contoning = + enabled && + m_top_surface_image_method_choice != nullptr && + m_top_surface_image_method_choice->GetSelection() == int(TextureMappingZone::TopSurfaceImageContoning); + if (contoning && m_modulation_mode_choice != nullptr) { + m_modulation_mode_choice->SetSelection(int(TextureMappingZone::ModulationPerimeterPathV2)); + m_modulation_mode_manually_changed = true; + update_modulation_mode_options_visibility(false); + } const bool same_layer = enabled && m_top_surface_image_method_choice != nullptr && @@ -3114,6 +3239,22 @@ private: m_top_surface_image_colored_top_layers_panel->Show(same_layer); if (m_top_surface_image_colored_top_layers_spin != nullptr) m_top_surface_image_colored_top_layers_spin->Enable(same_layer); + if (m_top_surface_contoning_panel != nullptr) + m_top_surface_contoning_panel->Show(contoning); + if (m_top_surface_contoning_angle_threshold_spin != nullptr) + m_top_surface_contoning_angle_threshold_spin->Enable(contoning); + if (m_top_surface_contoning_stack_layers_spin != nullptr) + m_top_surface_contoning_stack_layers_spin->Enable(contoning); + if (m_top_surface_contoning_min_feature_spin != nullptr) + m_top_surface_contoning_min_feature_spin->Enable(contoning); + if (m_top_surface_contoning_color_lower_surfaces_checkbox != nullptr) { + m_top_surface_contoning_color_lower_surfaces_checkbox->Show(contoning); + m_top_surface_contoning_color_lower_surfaces_checkbox->Enable(contoning); + } + if (m_top_surface_contoning_only_color_surface_infill_checkbox != nullptr) { + m_top_surface_contoning_only_color_surface_infill_checkbox->Show(contoning); + m_top_surface_contoning_only_color_surface_infill_checkbox->Enable(contoning); + } if (m_top_surface_image_fixed_coloring_filaments_checkbox != nullptr) m_top_surface_image_fixed_coloring_filaments_checkbox->Enable(enabled); layout_current_options_page(); @@ -3165,6 +3306,12 @@ private: wxPanel *m_top_surface_image_colored_top_layers_panel {nullptr}; wxSpinCtrl *m_top_surface_image_colored_top_layers_spin {nullptr}; wxCheckBox *m_top_surface_image_fixed_coloring_filaments_checkbox {nullptr}; + wxPanel *m_top_surface_contoning_panel {nullptr}; + wxSpinCtrlDouble *m_top_surface_contoning_angle_threshold_spin {nullptr}; + wxSpinCtrl *m_top_surface_contoning_stack_layers_spin {nullptr}; + wxSpinCtrlDouble *m_top_surface_contoning_min_feature_spin {nullptr}; + wxCheckBox *m_top_surface_contoning_color_lower_surfaces_checkbox {nullptr}; + wxCheckBox *m_top_surface_contoning_only_color_surface_infill_checkbox {nullptr}; wxCheckBox *m_use_legacy_fixed_color_mode_checkbox {nullptr}; wxCheckBox *m_minimum_visibility_offset_checkbox {nullptr}; wxSpinCtrl *m_minimum_visibility_offset_spin {nullptr}; @@ -8114,6 +8261,11 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.top_surface_image_max_line_width_mm, updated.top_surface_image_colored_top_layers, updated.top_surface_image_fixed_coloring_filaments, + updated.top_surface_contoning_angle_threshold_deg, + updated.top_surface_contoning_stack_layers, + updated.top_surface_contoning_min_feature_mm, + updated.top_surface_contoning_color_lower_surfaces, + updated.top_surface_contoning_only_color_surface_infill, bundle->texture_mapping_zones, bundle->texture_mapping_global_settings, wxGetApp().model().texture_mapping_prime_tower_image, @@ -8163,6 +8315,16 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.top_surface_image_max_line_width_mm = dlg.top_surface_image_max_line_width_mm(); updated.top_surface_image_colored_top_layers = dlg.top_surface_image_colored_top_layers(); updated.top_surface_image_fixed_coloring_filaments = dlg.top_surface_image_fixed_coloring_filaments(); + updated.top_surface_contoning_angle_threshold_deg = dlg.top_surface_contoning_angle_threshold_deg(); + updated.top_surface_contoning_stack_layers = dlg.top_surface_contoning_stack_layers(); + updated.top_surface_contoning_min_feature_mm = dlg.top_surface_contoning_min_feature_mm(); + updated.top_surface_contoning_color_lower_surfaces = dlg.top_surface_contoning_color_lower_surfaces(); + updated.top_surface_contoning_only_color_surface_infill = dlg.top_surface_contoning_only_color_surface_infill(); + if (updated.top_surface_image_printing_enabled && + updated.top_surface_image_printing_method == int(TextureMappingZone::TopSurfaceImageContoning)) { + updated.modulation_mode = int(TextureMappingZone::ModulationPerimeterPathV2); + updated.modulation_mode_manually_changed = true; + } if (updated.dithering_enabled) updated.compact_offset_mode = true; updated.minimum_visibility_offset_enabled = dlg.minimum_visibility_offset_enabled();