From 5841df8c198cad98e8316f8dc68684a6e05d9faa Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Mon, 18 May 2026 16:55:08 +0100 Subject: [PATCH] Implement perimeter path modulation method for texture mapping --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 6 +- src/libslic3r/LayerRegion.cpp | 367 +++- src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/TextureMapping.cpp | 4 + src/libslic3r/TextureMapping.hpp | 3 + src/libslic3r/TextureMappingOffset.cpp | 2181 ++++++++++++++++++++++++ src/libslic3r/TextureMappingOffset.hpp | 91 + src/slic3r/GUI/Plater.cpp | 8 + 10 files changed, 2660 insertions(+), 5 deletions(-) create mode 100644 src/libslic3r/TextureMappingOffset.cpp create mode 100644 src/libslic3r/TextureMappingOffset.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 232925e3362..d0d326cf476 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -474,6 +474,8 @@ set(lisbslic3r_sources TriangleSelector.hpp TriangleSetSampling.cpp TriangleSetSampling.hpp + TextureMappingOffset.cpp + TextureMappingOffset.hpp TextureMapping.cpp TextureMapping.hpp TriangulateWall.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 84f447776f9..2a9dc3841ed 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6350,17 +6350,17 @@ static float local_surface_stair_step_distance_for_gcode(const Layer *layer, static bool is_horizontal_overhang_gradient_row_for_gcode(const TextureMappingZone &zone) { - return zone.enabled && !zone.deleted && (zone.is_2d_gradient() || zone.is_image_texture()); + return zone.enabled && !zone.deleted && !zone.perimeter_path_modulation && (zone.is_2d_gradient() || zone.is_image_texture()); } static bool is_vertex_color_match_overhang_row_for_gcode(const TextureMappingZone &zone) { - return zone.enabled && !zone.deleted && zone.is_image_texture(); + return zone.enabled && !zone.deleted && !zone.perimeter_path_modulation && zone.is_image_texture(); } static bool is_2d_offset_gradient_row_for_gcode(const TextureMappingZone &zone) { - return zone.enabled && !zone.deleted && zone.is_2d_gradient(); + return zone.enabled && !zone.deleted && !zone.perimeter_path_modulation && zone.is_2d_gradient(); } static std::array unpack_rgba_u32(uint32_t packed_rgba) diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index 46397f32de9..d4e6a23a23f 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -8,16 +8,341 @@ #include "BoundingBox.hpp" #include "SVG.hpp" #include "TextureMapping.hpp" +#include "TextureMappingOffset.hpp" #include "Algorithm/RegionExpansion.hpp" +#include +#include +#include #include #include +#include #include #include namespace Slic3r { +struct PerimeterPathBoundarySample { + Point point; + double inward_x { 0.0 }; + double inward_y { 0.0 }; + float inset_mm { 0.f }; +}; + +static bool perimeter_texture_expolygons_contain_point(const ExPolygons &expolygons, const Point &point) +{ + return std::any_of(expolygons.begin(), expolygons.end(), [&point](const ExPolygon &expolygon) { + return expolygon.contains(point, true); + }); +} + +static std::vector perimeter_texture_build_erode_ladder(const ExPolygon &source, + float max_inset_mm, + float &step_mm) +{ + step_mm = std::clamp(max_inset_mm / 10.f, 0.025f, 0.08f); + const int level_count = std::clamp(int(std::ceil(max_inset_mm / std::max(step_mm, 1e-4f))) + 1, 1, 96); + std::vector ladder(static_cast(level_count)); + ladder.front().push_back(source); + for (int level = 1; level < level_count; ++level) { + const float distance_scaled = float(scale_(double(step_mm) * double(level))); + ladder[size_t(level)] = offset_ex(source, -distance_scaled); + if (ladder[size_t(level)].empty()) { + for (int fill = level + 1; fill < level_count; ++fill) + ladder[size_t(fill)].clear(); + break; + } + } + return ladder; +} + +static float perimeter_texture_safe_inset_for_sample(const ExPolygon &source, + const std::vector &erode_ladder, + float erode_step_mm, + const PerimeterPathBoundarySample &sample, + float desired_mm, + float max_inset_mm) +{ + const float clamped_desired = std::clamp(desired_mm, 0.f, max_inset_mm); + if (clamped_desired <= EPSILON) + return 0.f; + + auto point_at_inset = [&sample](float inset_mm) { + const double inset_scaled = scale_(double(inset_mm)); + return Point(coord_t(std::llround(double(sample.point.x()) + sample.inward_x * inset_scaled)), + coord_t(std::llround(double(sample.point.y()) + sample.inward_y * inset_scaled))); + }; + + auto inset_allowed = [&](float inset_mm) { + const Point candidate = point_at_inset(inset_mm); + if (!source.contains(candidate, true)) + return false; + const size_t level = std::min(erode_ladder.empty() ? size_t(0) : + size_t(std::floor(std::max(0.f, inset_mm - 0.01f) / std::max(erode_step_mm, 1e-4f))), + erode_ladder.empty() ? size_t(0) : erode_ladder.size() - 1); + if (level > 0) { + if (level >= erode_ladder.size() || erode_ladder[level].empty()) + return false; + if (!perimeter_texture_expolygons_contain_point(erode_ladder[level], candidate)) + return false; + } + return true; + }; + + if (inset_allowed(clamped_desired)) + return clamped_desired; + + float low = 0.f; + float high = clamped_desired; + for (int i = 0; i < 10; ++i) { + const float mid = 0.5f * (low + high); + if (inset_allowed(mid)) + low = mid; + else + high = mid; + } + return low; +} + +static std::vector perimeter_texture_sample_polygon_boundary( + const Polygon &polygon, + const TextureMappingOffsetContext &context, + const ExPolygon &source) +{ + std::vector samples; + const Points &points = polygon.points; + if (points.size() < 3) + return samples; + + float erode_step_mm = 0.f; + const std::vector erode_ladder = + perimeter_texture_build_erode_ladder(source, context.max_width_delta_mm, erode_step_mm); + const double pitch_scaled = scale_(context.high_resolution_texture_sampling ? 0.08 : 0.16); + + for (size_t idx = 0; idx < points.size(); ++idx) { + const Point &a = points[idx]; + const Point &b = points[(idx + 1) % points.size()]; + 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) + continue; + + const double inward_x = -dy / len; + const double inward_y = dx / len; + const int sample_count = std::clamp(int(std::ceil(len / std::max(pitch_scaled, 1.0))), 1, 4096); + for (int sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const double t = double(sample_idx) / double(sample_count); + PerimeterPathBoundarySample sample; + sample.point = Point(coord_t(std::llround(double(a.x()) + dx * t)), + coord_t(std::llround(double(a.y()) + dy * t))); + sample.inward_x = inward_x; + sample.inward_y = inward_y; + const float desired_mm = texture_mapping_offset_surface_inset_mm(context, sample.point, inward_x, inward_y); + sample.inset_mm = perimeter_texture_safe_inset_for_sample(source, + erode_ladder, + erode_step_mm, + sample, + desired_mm, + context.max_width_delta_mm); + samples.emplace_back(sample); + } + } + + if (samples.size() < 3) + return {}; + + auto sample_distance_mm = [](const PerimeterPathBoundarySample &lhs, const PerimeterPathBoundarySample &rhs) { + const double dx = double(lhs.point.x()) - double(rhs.point.x()); + const double dy = double(lhs.point.y()) - double(rhs.point.y()); + return unscale(std::hypot(dx, dy)); + }; + + auto smooth_insets = [&samples, &sample_distance_mm](float radius_mm) { + if (samples.size() < 3) + return; + radius_mm = std::clamp(radius_mm, 0.05f, 1.5f); + std::vector smoothed(samples.size(), 0.f); + for (size_t idx = 0; idx < samples.size(); ++idx) { + float weighted_sum = samples[idx].inset_mm; + float weight_sum = 1.f; + float distance_mm = 0.f; + for (size_t step = 1; step < samples.size(); ++step) { + const size_t prev = (idx + samples.size() - step) % samples.size(); + const size_t next_prev = (prev + 1) % samples.size(); + distance_mm += sample_distance_mm(samples[prev], samples[next_prev]); + if (distance_mm > radius_mm) + break; + const float weight = 1.f - distance_mm / radius_mm; + weighted_sum += samples[prev].inset_mm * weight; + weight_sum += weight; + } + distance_mm = 0.f; + for (size_t step = 1; step < samples.size(); ++step) { + const size_t next = (idx + step) % samples.size(); + const size_t prev_next = next == 0 ? samples.size() - 1 : next - 1; + distance_mm += sample_distance_mm(samples[prev_next], samples[next]); + if (distance_mm > radius_mm) + break; + const float weight = 1.f - distance_mm / radius_mm; + weighted_sum += samples[next].inset_mm * weight; + weight_sum += weight; + } + smoothed[idx] = weight_sum > EPSILON ? std::min(samples[idx].inset_mm, weighted_sum / weight_sum) : samples[idx].inset_mm; + } + for (size_t idx = 0; idx < samples.size(); ++idx) + samples[idx].inset_mm = smoothed[idx]; + }; + + smooth_insets(std::max(0.30f, 1.15f * context.base_outer_width_mm)); + + for (int pass = 0; pass < 4; ++pass) { + for (size_t idx = 0; idx < samples.size(); ++idx) { + const size_t prev = idx == 0 ? samples.size() - 1 : idx - 1; + const float limit = sample_distance_mm(samples[idx], samples[prev]) * 0.35f + 0.015f; + if (samples[idx].inset_mm > samples[prev].inset_mm + limit) + samples[idx].inset_mm = samples[prev].inset_mm + limit; + } + for (size_t idx = samples.size(); idx-- > 0;) { + const size_t next = (idx + 1) % samples.size(); + const float limit = sample_distance_mm(samples[idx], samples[next]) * 0.35f + 0.015f; + if (samples[idx].inset_mm > samples[next].inset_mm + limit) + samples[idx].inset_mm = samples[next].inset_mm + limit; + } + } + + smooth_insets(std::max(0.20f, 0.45f * context.base_outer_width_mm)); + + return samples; +} + +static Polygon perimeter_texture_moved_polygon_from_samples(const std::vector &samples) +{ + Polygon polygon; + polygon.points.reserve(samples.size()); + Point last_point; + bool has_last = false; + for (const PerimeterPathBoundarySample &sample : samples) { + const double inset_scaled = scale_(double(sample.inset_mm)); + Point moved(coord_t(std::llround(double(sample.point.x()) + sample.inward_x * inset_scaled)), + coord_t(std::llround(double(sample.point.y()) + sample.inward_y * inset_scaled))); + if (!has_last || moved != last_point) { + polygon.points.emplace_back(moved); + last_point = moved; + has_last = true; + } + } + if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back()) + polygon.points.pop_back(); + remove_same_neighbor(polygon); + return polygon; +} + +static ExPolygons perimeter_texture_modulated_expolygon(const ExPolygon &source, + const TextureMappingOffsetContext &context, + bool &ok) +{ + ok = false; + if (source.empty() || source.contour.points.size() < 3) + return {}; + + const double source_area = std::abs(source.area()); + if (!std::isfinite(source_area) || source_area <= 0.0) + return {}; + + const std::vector contour_samples = + perimeter_texture_sample_polygon_boundary(source.contour, context, source); + if (contour_samples.size() < 3) + return {}; + + bool has_meaningful_inset = false; + for (const PerimeterPathBoundarySample &sample : contour_samples) { + if (sample.inset_mm > 0.002f) { + has_meaningful_inset = true; + break; + } + } + std::vector> hole_samples; + hole_samples.reserve(source.holes.size()); + for (const Polygon &hole : source.holes) { + hole_samples.emplace_back(perimeter_texture_sample_polygon_boundary(hole, context, source)); + if (hole_samples.back().size() < 3) + return {}; + for (const PerimeterPathBoundarySample &sample : hole_samples.back()) { + if (sample.inset_mm > 0.002f) { + has_meaningful_inset = true; + break; + } + } + } + if (!has_meaningful_inset) { + ok = true; + return ExPolygons{ source }; + } + + ExPolygon moved; + moved.contour = perimeter_texture_moved_polygon_from_samples(contour_samples); + if (moved.contour.points.size() < 3) + return {}; + moved.contour.make_counter_clockwise(); + moved.holes.reserve(hole_samples.size()); + for (const std::vector &samples : hole_samples) { + Polygon hole = perimeter_texture_moved_polygon_from_samples(samples); + if (hole.points.size() < 3) + return {}; + hole.make_clockwise(); + moved.holes.emplace_back(std::move(hole)); + } + + ExPolygons simplified = moved.simplify(scale_(0.006)); + if (simplified.empty()) + return {}; + + ExPolygons source_only; + source_only.emplace_back(source); + ExPolygons clipped = intersection_ex(simplified, source_only); + if (clipped.empty()) + return {}; + remove_same_neighbor(clipped); + + const double clipped_area = area(clipped); + if (!std::isfinite(clipped_area) || clipped_area <= source_area * 0.10) + return {}; + if (clipped_area > source_area * 1.002) + return {}; + + ok = true; + return clipped; +} + +static SurfaceCollection perimeter_path_modulated_surfaces(const LayerRegion &layer_region, + const SurfaceCollection &slices, + const TextureMappingZone &zone, + unsigned int texture_zone_id) +{ + const Layer *layer = layer_region.layer(); + if (layer == nullptr || layer->object() == nullptr) + return slices; + + std::optional context = + build_texture_mapping_offset_context_for_layer(*layer->object(), *layer, zone, texture_zone_id); + if (!context) + return slices; + + SurfaceCollection out; + out.surfaces.reserve(slices.surfaces.size()); + for (const Surface &surface : slices.surfaces) { + bool ok = false; + ExPolygons modulated = perimeter_texture_modulated_expolygon(surface.expolygon, *context, ok); + if (ok && !modulated.empty()) + out.append(std::move(modulated), surface); + else + out.surfaces.emplace_back(surface); + } + return out; +} + static bool region_uses_overhang_texture_mapping(const Print &print, const PrintRegionConfig ®ion_config) { const int filament_id = region_config.wall_filament.value; @@ -28,6 +353,26 @@ static bool region_uses_overhang_texture_mapping(const Print &print, const Print return zone != nullptr && zone->enabled && !zone->deleted && (zone->is_2d_gradient() || zone->is_image_texture()); } +static const TextureMappingZone *perimeter_path_modulation_zone_for_region(const Print &print, + const PrintRegionConfig ®ion_config, + unsigned int &texture_zone_id) +{ + const int filament_id = region_config.wall_filament.value; + if (filament_id <= 0) + return nullptr; + + texture_zone_id = unsigned(filament_id); + const TextureMappingZone *zone = print.texture_mapping_manager().zone_from_id(texture_zone_id); + if (zone == nullptr || + !zone->enabled || + zone->deleted || + !zone->perimeter_path_modulation || + (!zone->is_2d_gradient() && !zone->is_image_texture())) + return nullptr; + + return zone; +} + Flow LayerRegion::flow(FlowRole role) const { return this->flow(role, m_layer->height); @@ -93,9 +438,20 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe (this->layer()->id() >= size_t(region_config.bottom_shell_layers.value) && this->layer()->print_z >= region_config.bottom_shell_thickness - EPSILON); + SurfaceCollection modulated_slices; + const SurfaceCollection *perimeter_slices = &slices; + unsigned int perimeter_texture_zone_id = 0; + bool use_perimeter_path_modulation = false; + if (const TextureMappingZone *zone = + perimeter_path_modulation_zone_for_region(*this->layer()->object()->print(), region_config, perimeter_texture_zone_id)) { + modulated_slices = perimeter_path_modulated_surfaces(*this, slices, *zone, perimeter_texture_zone_id); + perimeter_slices = &modulated_slices; + use_perimeter_path_modulation = true; + } + PerimeterGenerator g( // input: - &slices, + perimeter_slices, &compatible_regions, this->layer()->height, this->layer()->slice_z, @@ -125,6 +481,15 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe g.layer_id = (int)this->layer()->id(); g.ext_perimeter_flow = this->flow(frExternalPerimeter); + if (use_perimeter_path_modulation) { + const Flow normal_ext_perimeter_flow = g.ext_perimeter_flow; + const float texture_external_width_mm = + std::max(0.05f, float(print_config.texture_mapping_outer_wall_gradient_max_line_width.value)); + const float min_width_for_positive_spacing_mm = + std::max(0.01f, float(this->layer()->height) * float(1. - 0.25 * PI) + 1e-4f); + g.ext_perimeter_flow = normal_ext_perimeter_flow.with_width(std::max(texture_external_width_mm, min_width_for_positive_spacing_mm)); + g.ext_perimeter_flow.set_spacing(std::min(g.ext_perimeter_flow.spacing(), normal_ext_perimeter_flow.spacing())); + } g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges); g.solid_infill_flow = this->flow(frSolidInfill); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 3afa1c7e017..0053365147b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -512,6 +512,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "texture_mapping_outer_wall_gradient_min_line_width" || opt_key == "texture_mapping_definitions" || opt_key == "texture_mapping_global_settings") { + osteps.emplace_back(posPerimeters); steps.emplace_back(psWipeTower); steps.emplace_back(psSkirtBrim); } else if ( diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index dcb00556723..56a12f04f66 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1169,6 +1169,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "texture_mapping_outer_wall_gradient_max_line_width" || opt_key == "texture_mapping_outer_wall_gradient_min_line_width" || opt_key == "texture_mapping_definitions" + || opt_key == "texture_mapping_background_color" || opt_key == "texture_mapping_global_settings") { steps.emplace_back(posSlice); } else if ( @@ -1423,7 +1424,6 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "support_interface_flow_ratio" || opt_key == "brim_flow_ratio" || opt_key == "filament_flow_ratio" - || opt_key == "texture_mapping_background_color" || opt_key == "scarf_joint_flow_ratio" || opt_key == "spiral_starting_flow_ratio" || opt_key == "spiral_finishing_flow_ratio") { diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index 1783d89a652..52b312f49c3 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -794,6 +794,7 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const reduce_outer_surface_texture == rhs.reduce_outer_surface_texture && seam_hiding == rhs.seam_hiding && nonlinear_offset_adjustment == rhs.nonlinear_offset_adjustment && + perimeter_path_modulation == rhs.perimeter_path_modulation && 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 && @@ -1043,6 +1044,7 @@ std::string TextureMappingManager::serialize_entries() texture["reduce_outer_surface_texture"] = zone.reduce_outer_surface_texture; texture["hide_seams"] = zone.seam_hiding; texture["nonlinear_offset_adjustment"] = zone.nonlinear_offset_adjustment; + texture["perimeter_path_modulation"] = zone.perimeter_path_modulation; 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"] = zone.high_speed_image_texture_sampling; @@ -1174,6 +1176,8 @@ void TextureMappingManager::load_entries(const std::string &serialized, zone.reduce_outer_surface_texture = texture.value("reduce_outer_surface_texture", false); zone.seam_hiding = texture.value("hide_seams", false); zone.nonlinear_offset_adjustment = texture.value("nonlinear_offset_adjustment", false); + zone.perimeter_path_modulation = + texture.value("perimeter_path_modulation", TextureMappingZone::DefaultPerimeterPathModulation); 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 9dcf3a355f2..149262e5c51 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -94,6 +94,7 @@ struct TextureMappingZone static constexpr bool DefaultReduceOuterSurfaceTexture = false; static constexpr bool DefaultSeamHiding = false; static constexpr bool DefaultNonlinearOffsetAdjustment = false; + static constexpr bool DefaultPerimeterPathModulation = false; static constexpr bool DefaultCompactOffsetMode = true; static constexpr bool DefaultUseLegacyFixedColorMode = false; static constexpr bool DefaultHighSpeedImageTextureSampling = true; @@ -138,6 +139,7 @@ struct TextureMappingZone bool reduce_outer_surface_texture = DefaultReduceOuterSurfaceTexture; bool seam_hiding = DefaultSeamHiding; bool nonlinear_offset_adjustment = DefaultNonlinearOffsetAdjustment; + bool perimeter_path_modulation = DefaultPerimeterPathModulation; bool compact_offset_mode = DefaultCompactOffsetMode; bool use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; bool high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; @@ -183,6 +185,7 @@ struct TextureMappingZone reduce_outer_surface_texture = DefaultReduceOuterSurfaceTexture; seam_hiding = DefaultSeamHiding; nonlinear_offset_adjustment = DefaultNonlinearOffsetAdjustment; + perimeter_path_modulation = DefaultPerimeterPathModulation; compact_offset_mode = DefaultCompactOffsetMode; use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; diff --git a/src/libslic3r/TextureMappingOffset.cpp b/src/libslic3r/TextureMappingOffset.cpp new file mode 100644 index 00000000000..d14c450a733 --- /dev/null +++ b/src/libslic3r/TextureMappingOffset.cpp @@ -0,0 +1,2181 @@ +// original author: sentientstardust + +#include "TextureMappingOffset.hpp" + +#include "BoundingBox.hpp" +#include "Color.hpp" +#include "ColorSolver.hpp" +#include "Config.hpp" +#include "Geometry.hpp" +#include "ImageMapRawFilamentOffsetAtlas.hpp" +#include "Layer.hpp" +#include "Model.hpp" +#include "Print.hpp" +#include "TriangleMesh.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +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 }; +}; + +struct WeightedTextureSample { + float x_mm { 0.f }; + float y_mm { 0.f }; + std::array rgba { { 0.f, 0.f, 0.f, 1.f } }; + std::vector raw_component_weights; + bool raw_component_weights_from_texture { false }; + float weight { 0.f }; +}; + +struct LayerPlaneSamplePoint { + Vec3d p; + Vec3f barycentric; +}; + +struct TransmissionDistanceCalibrationContext { + bool enabled { false }; + int mode { int(TextureMappingZone::TDCalibrationNone) }; + std::vector own_width_factors; + std::vector neighbor_opacity_ratios; +}; + +float clamp01f(float v) +{ + if (!std::isfinite(v)) + return 0.f; + return std::clamp(v, 0.f, 1.f); +} + +int texture_mapping_color_hex_digit(char ch) +{ + return ch >= '0' && ch <= '9' ? ch - '0' : + ch >= 'a' && ch <= 'f' ? ch - 'a' + 10 : + ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1; +} + +std::optional> parse_texture_mapping_color_hex(const std::string &text) +{ + if (text.empty()) + return std::nullopt; + + const size_t hash_pos = text.find('#'); + const size_t start = hash_pos == std::string::npos ? 0 : hash_pos + 1; + if (start + 6 > text.size()) + return std::nullopt; + + uint32_t packed = 0; + for (size_t idx = 0; idx < 6; ++idx) { + const int value = texture_mapping_color_hex_digit(text[start + idx]); + if (value < 0) + return std::nullopt; + packed = (packed << 4) | uint32_t(value); + } + + uint32_t alpha = 255; + if (start + 8 <= text.size()) { + alpha = 0; + for (size_t idx = 6; idx < 8; ++idx) { + const int value = texture_mapping_color_hex_digit(text[start + idx]); + if (value < 0) + return std::nullopt; + alpha = (alpha << 4) | uint32_t(value); + } + } + + return std::array { + clamp01f(float((packed >> 16) & 0xFFu) / 255.f), + clamp01f(float((packed >> 8) & 0xFFu) / 255.f), + clamp01f(float(packed & 0xFFu) / 255.f), + clamp01f(float(alpha & 0xFFu) / 255.f) + }; +} + +std::array unpack_rgba_u32(uint32_t packed_rgba) +{ + const float r = float((packed_rgba >> 24) & 0xFFu) / 255.f; + const float g = float((packed_rgba >> 16) & 0xFFu) / 255.f; + const float b = float((packed_rgba >> 8) & 0xFFu) / 255.f; + const float a = float(packed_rgba & 0xFFu) / 255.f; + return { clamp01f(r), clamp01f(g), clamp01f(b), clamp01f(a) }; +} + +std::optional> texture_mapping_background_color_from_config(const ModelConfigObject &config) +{ + static constexpr const char *key = "texture_mapping_background_color"; + if (!config.has(key)) + return std::nullopt; + + const ConfigOptionString *opt = dynamic_cast(config.option(key)); + if (opt == nullptr) + return std::nullopt; + + std::optional> color = parse_texture_mapping_color_hex(opt->value); + if (color) + (*color)[3] = 1.f; + return color; +} + +std::optional> texture_mapping_background_color_from_metadata(const ColorFacetsAnnotation &annotation) +{ + const std::string &metadata = annotation.metadata_json(); + const std::string key = "\"background_color\":\"#"; + const size_t start = metadata.find(key); + if (start == std::string::npos || start + key.size() + 8 > metadata.size()) + return std::nullopt; + + std::optional> color = parse_texture_mapping_color_hex(metadata.substr(start + key.size() - 1, 9)); + if (color) + (*color)[3] = 1.f; + return color; +} + +std::array texture_mapping_background_color(const ModelVolume &volume) +{ + if (std::optional> color = texture_mapping_background_color_from_config(volume.config)) + return *color; + if (volume.get_object() != nullptr) { + if (std::optional> color = texture_mapping_background_color_from_config(volume.get_object()->config)) + return *color; + } + if (std::optional> color = texture_mapping_background_color_from_metadata(volume.texture_mapping_color_facets)) + return *color; + return { 1.f, 1.f, 1.f, 1.f }; +} + +std::array composite_rgba_over_background(const std::array &rgba, + const std::array &background) +{ + const float alpha = clamp01f(rgba[3]); + return { + clamp01f(rgba[0] * alpha + background[0] * (1.f - alpha)), + clamp01f(rgba[1] * alpha + background[1] * (1.f - alpha)), + clamp01f(rgba[2] * alpha + background[2] * (1.f - alpha)), + 1.f + }; +} + +float wrap_repeat01(float uv) +{ + if (!std::isfinite(uv)) + return 0.f; + + constexpr float eps = 1e-6f; + if (uv >= -eps && uv <= 1.f + eps) + return std::clamp(uv, 0.f, 1.f); + + float wrapped = uv - std::floor(uv); + if (wrapped < 0.f) + wrapped += 1.f; + return wrapped; +} + +std::array sample_texture_rgba_bilinear(const std::vector &rgba, + uint32_t width, + uint32_t height, + float u, + float v) +{ + if (width == 0 || height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return { 0.f, 0.f, 0.f, 1.f }; + + const float uu = wrap_repeat01(u); + const float vv = wrap_repeat01(v); + const float x = uu * float(width > 1 ? width - 1 : 0); + const float y = vv * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&rgba, width](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * 4 + channel; + return float(rgba[idx]) / 255.f; + }; + + std::array out {}; + for (size_t c = 0; c < 4; ++c) { + const float c00 = sample_channel(x0, y0, c); + const float c10 = sample_channel(x1, y0, c); + const float c01 = sample_channel(x0, y1, c); + const float c11 = sample_channel(x1, y1, c); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + out[c] = clamp01f(cx0 + (cx1 - cx0) * ty); + } + return out; +} + +std::vector sample_texture_raw_offsets_bilinear(const std::vector &offsets, + uint32_t width, + uint32_t height, + uint32_t channels, + float u, + float v) +{ + std::vector out; + if (width == 0 || height == 0 || channels == 0 || + offsets.size() < size_t(width) * size_t(height) * size_t(channels)) + return out; + + const float uu = wrap_repeat01(u); + const float vv = wrap_repeat01(v); + const float x = uu * float(width > 1 ? width - 1 : 0); + const float y = vv * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&offsets, width, channels](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * size_t(channels) + channel; + return float(offsets[idx]) / 255.f; + }; + + out.assign(size_t(channels), 0.f); + for (size_t c = 0; c < out.size(); ++c) { + const float c00 = sample_channel(x0, y0, c); + const float c10 = sample_channel(x1, y0, c); + const float c01 = sample_channel(x0, y1, c); + const float c11 = sample_channel(x1, y1, c); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + out[c] = clamp01f(cx0 + (cx1 - cx0) * ty); + } + return out; +} + +std::array raw_offset_preview_rgba(const std::vector &offsets) +{ + if (offsets.empty()) + return { 0.f, 0.f, 0.f, 1.f }; + if (offsets.size() == 1) + return { offsets[0], offsets[0], offsets[0], 1.f }; + return { + offsets.size() > 0 ? offsets[0] : 0.f, + offsets.size() > 1 ? offsets[1] : 0.f, + offsets.size() > 2 ? offsets[2] : 0.f, + 1.f + }; +} + +float color_distance_sq(const std::array &lhs, const std::array &rhs) +{ + const std::array lhs_oklab = color_solver_oklab_from_srgb(lhs); + const std::array rhs_oklab = color_solver_oklab_from_srgb(rhs); + const float dl = lhs_oklab[0] - rhs_oklab[0]; + const float da = lhs_oklab[1] - rhs_oklab[1]; + const float db = lhs_oklab[2] - rhs_oklab[2]; + return dl * dl + da * da + db * db; +} + +std::array raw_filament_channel_color(const ImageMapRawFilament &filament, size_t channel_idx) +{ + const std::string key = image_map_raw_filament_channel_key(filament, channel_idx); + if (key == "C") + return { { 0.f, 1.f, 1.f } }; + if (key == "M") + return { { 1.f, 0.f, 1.f } }; + if (key == "Y") + return { { 1.f, 1.f, 0.f } }; + if (key == "K") + return { { 0.f, 0.f, 0.f } }; + if (key == "W") + return { { 1.f, 1.f, 1.f } }; + if (key == "R") + return { { 1.f, 0.f, 0.f } }; + if (key == "G") + return { { 0.f, 1.f, 0.f } }; + if (key == "B") + return { { 0.f, 0.f, 1.f } }; + if (!filament.hex.empty()) { + const std::optional> parsed = parse_texture_mapping_color_hex(filament.hex); + if (parsed) + return { { (*parsed)[0], (*parsed)[1], (*parsed)[2] } }; + } + return { { 1.f, 1.f, 1.f } }; +} + +std::vector raw_filament_color_mode_channel_keys(int filament_color_mode, size_t component_count) +{ + std::vector keys; + switch (std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorRGBKW))) { + case int(TextureMappingZone::FilamentColorRGB): keys = { "R", "G", "B" }; break; + case int(TextureMappingZone::FilamentColorCMY): keys = { "C", "M", "Y" }; break; + case int(TextureMappingZone::FilamentColorCMYK): keys = { "C", "M", "Y", "K" }; break; + case int(TextureMappingZone::FilamentColorCMYW): keys = { "C", "M", "Y", "W" }; break; + case int(TextureMappingZone::FilamentColorRGBK): keys = { "R", "G", "B", "K" }; break; + case int(TextureMappingZone::FilamentColorRGBW): keys = { "R", "G", "B", "W" }; break; + case int(TextureMappingZone::FilamentColorBW): keys = { "K", "W" }; break; + case int(TextureMappingZone::FilamentColorCMYKW): keys = { "C", "M", "Y", "K", "W" }; break; + case int(TextureMappingZone::FilamentColorRGBKW): keys = { "R", "G", "B", "K", "W" }; break; + default: break; + } + if (keys.size() > component_count) + keys.resize(component_count); + return keys; +} + +std::vector raw_component_source_channels(const std::string &metadata_json, + uint32_t source_channels, + int filament_color_mode, + size_t component_count, + const std::vector> &component_colors) +{ + if (source_channels == 0 || component_count == 0) + return {}; + + const size_t sentinel = std::numeric_limits::max(); + std::vector mapping(component_count, sentinel); + const std::vector filaments = + image_map_raw_filaments_from_metadata_json(metadata_json, source_channels); + if (filaments.size() != size_t(source_channels)) + return {}; + + std::vector source_keys(static_cast(source_channels)); + std::vector> source_colors(static_cast(source_channels)); + for (size_t channel = 0; channel < filaments.size(); ++channel) { + const std::string key = image_map_raw_filament_channel_key(filaments[channel], channel); + if (key.size() == 1 && image_map_raw_filament_is_standard_color(key)) + source_keys[channel] = key; + source_colors[channel] = raw_filament_channel_color(filaments[channel], channel); + } + + const std::vector target_keys = raw_filament_color_mode_channel_keys(filament_color_mode, component_count); + if (!target_keys.empty()) { + std::vector used(static_cast(source_channels), 0); + for (size_t component_idx = 0; component_idx < target_keys.size(); ++component_idx) { + for (size_t channel = 0; channel < source_keys.size(); ++channel) { + if (used[channel] == 0 && source_keys[channel] == target_keys[component_idx]) { + mapping[component_idx] = channel; + used[channel] = 1; + break; + } + } + } + + const float max_match_distance_sq = + TextureMappingManager::poor_color_match_distance() * TextureMappingManager::poor_color_match_distance(); + for (size_t component_idx = 0; component_idx < target_keys.size(); ++component_idx) { + if (mapping[component_idx] != sentinel) + continue; + const std::array target_color = + raw_filament_channel_color({ 0, target_keys[component_idx], std::string() }, component_idx); + size_t best_channel = size_t(source_channels); + float best_distance_sq = std::numeric_limits::max(); + for (size_t channel = 0; channel < source_colors.size(); ++channel) { + if (used[channel] != 0) + continue; + const float distance_sq = color_distance_sq(source_colors[channel], target_color); + if (distance_sq < best_distance_sq) { + best_distance_sq = distance_sq; + best_channel = channel; + } + } + if (best_channel < source_colors.size() && best_distance_sq <= max_match_distance_sq) { + mapping[component_idx] = best_channel; + used[best_channel] = 1; + } + } + return mapping; + } + + if (component_colors.size() == component_count) { + struct Candidate { + float distance_sq { 0.f }; + size_t component_idx { 0 }; + size_t source_channel { 0 }; + }; + + std::vector candidates; + candidates.reserve(component_count * size_t(source_channels)); + for (size_t channel = 0; channel < filaments.size(); ++channel) { + for (size_t component_idx = 0; component_idx < component_count; ++component_idx) + candidates.push_back({ color_distance_sq(component_colors[component_idx], source_colors[channel]), component_idx, channel }); + } + std::sort(candidates.begin(), candidates.end(), [](const Candidate &lhs, const Candidate &rhs) { + return lhs.distance_sq < rhs.distance_sq; + }); + + std::vector used_components(component_count, 0); + std::vector used_sources(static_cast(source_channels), 0); + for (const Candidate &candidate : candidates) { + if (used_components[candidate.component_idx] != 0 || used_sources[candidate.source_channel] != 0) + continue; + mapping[candidate.component_idx] = candidate.source_channel; + used_components[candidate.component_idx] = 1; + used_sources[candidate.source_channel] = 1; + } + } + + const bool has_mapping = std::any_of(mapping.begin(), mapping.end(), [sentinel](size_t value) { return value != sentinel; }); + return has_mapping ? mapping : std::vector{}; +} + +std::vector map_raw_sample_to_components(const std::vector &raw_sample, + const std::vector &component_source_channels) +{ + if (component_source_channels.empty()) + return {}; + const size_t sentinel = std::numeric_limits::max(); + std::vector mapped(component_source_channels.size(), 0.f); + for (size_t component_idx = 0; component_idx < component_source_channels.size(); ++component_idx) { + const size_t source_channel = component_source_channels[component_idx]; + if (source_channel != sentinel && source_channel < raw_sample.size()) + mapped[component_idx] = raw_sample[source_channel]; + } + return mapped; +} + +std::vector> fixed_color_generic_solver_component_colors(int filament_color_mode) +{ + switch (std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorRGBKW))) { + case int(TextureMappingZone::FilamentColorRGB): + return { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } } }; + case int(TextureMappingZone::FilamentColorCMY): + return { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } } }; + case int(TextureMappingZone::FilamentColorCMYK): + return { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 0.f, 0.f, 0.f } } }; + case int(TextureMappingZone::FilamentColorCMYW): + return { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + case int(TextureMappingZone::FilamentColorRGBK): + return { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 0.f, 0.f, 0.f } } }; + case int(TextureMappingZone::FilamentColorRGBW): + return { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 1.f, 1.f, 1.f } } }; + case int(TextureMappingZone::FilamentColorBW): + return { { { 0.f, 0.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + case int(TextureMappingZone::FilamentColorCMYKW): + return { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 0.f, 0.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + case int(TextureMappingZone::FilamentColorRGBKW): + return { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 0.f, 0.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + default: + return {}; + } +} + +std::vector best_matching_component_indices_for_semantic_colors( + const std::vector> &component_colors, + const std::vector> &semantic_colors) +{ + if (component_colors.size() != semantic_colors.size() || component_colors.empty() || component_colors.size() > 8) + return {}; + + std::vector permutation(component_colors.size(), 0); + std::iota(permutation.begin(), permutation.end(), size_t(0)); + + std::vector best_permutation = permutation; + float best_error = std::numeric_limits::max(); + do { + float error = 0.f; + for (size_t role_idx = 0; role_idx < semantic_colors.size(); ++role_idx) + error += color_distance_sq(component_colors[permutation[role_idx]], semantic_colors[role_idx]); + + if (error < best_error) { + best_error = error; + best_permutation = permutation; + } + } while (std::next_permutation(permutation.begin(), permutation.end())); + + return best_permutation; +} + +std::vector semantic_component_indices(const std::vector> &component_colors, + int filament_color_mode, + bool force_sequential_filaments) +{ + if (force_sequential_filaments) + return {}; + + std::vector> semantic_colors; + switch (filament_color_mode) { + case int(TextureMappingZone::FilamentColorRGB): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMY): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYK): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 0.f, 0.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYW): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorRGBK): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 0.f, 0.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorRGBW): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 1.f, 1.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYKW): + semantic_colors = { + { { 0.f, 1.f, 1.f } }, + { { 1.f, 0.f, 1.f } }, + { { 1.f, 1.f, 0.f } }, + { { 0.f, 0.f, 0.f } }, + { { 1.f, 1.f, 1.f } } + }; + break; + case int(TextureMappingZone::FilamentColorRGBKW): + semantic_colors = { + { { 1.f, 0.f, 0.f } }, + { { 0.f, 1.f, 0.f } }, + { { 0.f, 0.f, 1.f } }, + { { 0.f, 0.f, 0.f } }, + { { 1.f, 1.f, 1.f } } + }; + break; + default: + return {}; + } + + return best_matching_component_indices_for_semantic_colors(component_colors, semantic_colors); +} + +std::vector optimized_component_weights(const std::array &target_rgb, + size_t component_count, + int filament_color_mode, + const std::vector> &component_colors, + bool force_sequential_filaments) +{ + const int mode = std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorRGBKW)); + if (mode == int(TextureMappingZone::FilamentColorAny)) + return {}; + + auto strength = [](float value) { return clamp01f(std::pow(std::max(0.f, value), 0.85f)); }; + auto safe_div = [](float numerator, float denominator) { + if (denominator <= EPSILON) + return 0.f; + return clamp01f(numerator / denominator); + }; + const std::vector semantic_indices = + semantic_component_indices(component_colors, mode, force_sequential_filaments); + const auto component_index_for_role = [&semantic_indices](size_t role_idx) { + if (role_idx < semantic_indices.size()) + return semantic_indices[role_idx]; + return role_idx; + }; + + const float r = clamp01f(target_rgb[0]); + const float g = clamp01f(target_rgb[1]); + const float b = clamp01f(target_rgb[2]); + const float whiteness = std::min({ r, g, b }); + const float darkness = 1.f - std::max({ r, g, b }); + std::vector weights(component_count, 0.f); + + if (mode == int(TextureMappingZone::FilamentColorRGB) && component_count == 3) { + weights[component_index_for_role(0)] = strength(r); + weights[component_index_for_role(1)] = strength(g); + weights[component_index_for_role(2)] = strength(b); + return weights; + } + if (mode == int(TextureMappingZone::FilamentColorCMY) && component_count == 3) { + weights[component_index_for_role(0)] = strength(1.f - r); + weights[component_index_for_role(1)] = strength(1.f - g); + weights[component_index_for_role(2)] = strength(1.f - b); + return weights; + } + if (mode == int(TextureMappingZone::FilamentColorBW) && component_count == 2) { + const float gray = clamp01f(0.2126f * r + 0.7152f * g + 0.0722f * b); + size_t black_component_idx = 0; + size_t white_component_idx = 1; + if (!force_sequential_filaments && component_colors.size() >= 2) { + const float lum0 = 0.2126f * component_colors[0][0] + 0.7152f * component_colors[0][1] + 0.0722f * component_colors[0][2]; + const float lum1 = 0.2126f * component_colors[1][0] + 0.7152f * component_colors[1][1] + 0.0722f * component_colors[1][2]; + if (lum0 > lum1) { + black_component_idx = 1; + white_component_idx = 0; + } + } + weights[black_component_idx] = strength(gray >= 0.5f ? 2.f * (1.f - gray) : 1.f); + weights[white_component_idx] = strength(gray <= 0.5f ? 2.f * gray : 1.f); + return weights; + } + if ((mode == int(TextureMappingZone::FilamentColorCMYK) || + mode == int(TextureMappingZone::FilamentColorRGBK) || + mode == int(TextureMappingZone::FilamentColorCMYW) || + mode == int(TextureMappingZone::FilamentColorRGBW)) && component_count == 4) { + if (mode == int(TextureMappingZone::FilamentColorCMYK)) { + const float k = clamp01f(darkness); + const float inv = 1.f - k; + weights[component_index_for_role(0)] = strength(safe_div(1.f - r - k, inv)); + weights[component_index_for_role(1)] = strength(safe_div(1.f - g - k, inv)); + weights[component_index_for_role(2)] = strength(safe_div(1.f - b - k, inv)); + weights[component_index_for_role(3)] = strength(k); + } else if (mode == int(TextureMappingZone::FilamentColorRGBK)) { + const float k = clamp01f(darkness); + const float inv = 1.f - k; + weights[component_index_for_role(0)] = strength(safe_div(r - k, inv)); + weights[component_index_for_role(1)] = strength(safe_div(g - k, inv)); + weights[component_index_for_role(2)] = strength(safe_div(b - k, inv)); + weights[component_index_for_role(3)] = strength(k); + } else if (mode == int(TextureMappingZone::FilamentColorCMYW)) { + const float inv = 1.f - whiteness; + const float r_no_w = safe_div(r - whiteness, inv); + const float g_no_w = safe_div(g - whiteness, inv); + const float b_no_w = safe_div(b - whiteness, inv); + weights[component_index_for_role(0)] = strength(clamp01f((1.f - r_no_w) * inv)); + weights[component_index_for_role(1)] = strength(clamp01f((1.f - g_no_w) * inv)); + weights[component_index_for_role(2)] = strength(clamp01f((1.f - b_no_w) * inv)); + weights[component_index_for_role(3)] = clamp01f(std::pow(whiteness, 1.35f)); + } else { + const float inv = 1.f - whiteness; + weights[component_index_for_role(0)] = strength(safe_div(r - whiteness, inv)); + weights[component_index_for_role(1)] = strength(safe_div(g - whiteness, inv)); + weights[component_index_for_role(2)] = strength(safe_div(b - whiteness, inv)); + weights[component_index_for_role(3)] = strength(whiteness); + } + return weights; + } + if ((mode == int(TextureMappingZone::FilamentColorCMYKW) || + mode == int(TextureMappingZone::FilamentColorRGBKW)) && component_count == 5) { + const float chroma = std::max(0.f, 1.f - darkness - whiteness); + if (mode == int(TextureMappingZone::FilamentColorCMYKW)) { + weights[component_index_for_role(0)] = strength(chroma <= EPSILON ? 0.f : 1.f - safe_div(r - whiteness, chroma)); + weights[component_index_for_role(1)] = strength(chroma <= EPSILON ? 0.f : 1.f - safe_div(g - whiteness, chroma)); + weights[component_index_for_role(2)] = strength(chroma <= EPSILON ? 0.f : 1.f - safe_div(b - whiteness, chroma)); + } else { + weights[component_index_for_role(0)] = strength(safe_div(r - whiteness, chroma)); + weights[component_index_for_role(1)] = strength(safe_div(g - whiteness, chroma)); + weights[component_index_for_role(2)] = strength(safe_div(b - whiteness, chroma)); + } + weights[component_index_for_role(3)] = strength(darkness); + weights[component_index_for_role(4)] = strength(whiteness); + return weights; + } + return {}; +} + +float apply_texture_tone_gamma(float channel, float tone_gamma) +{ + const float safe_channel = clamp01f(channel); + const float safe_gamma = + (!std::isfinite(tone_gamma) || tone_gamma <= 0.f) ? 1.f : std::clamp(tone_gamma, 0.5f, 3.f); + if (std::abs(safe_gamma - 1.f) <= 1e-5f) + return safe_channel; + return clamp01f(std::pow(safe_channel, 1.f / safe_gamma)); +} + +void apply_texture_contrast_to_components(std::vector &component_weights, + float contrast_factor, + size_t mapped_component_count) +{ + const size_t count = std::min(mapped_component_count, component_weights.size()); + if (count == 0) + return; + + float mean_weight = 0.f; + for (size_t idx = 0; idx < count; ++idx) + mean_weight += clamp01f(component_weights[idx]); + mean_weight /= float(count); + + for (size_t idx = 0; idx < count; ++idx) { + const float safe_weight = clamp01f(component_weights[idx]); + component_weights[idx] = clamp01f(mean_weight + (safe_weight - mean_weight) * contrast_factor); + } +} + +std::array unwrap_triangle_uvs(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2) +{ + std::array out { uv0, uv1, uv2 }; + auto unwrap_axis = [&out](bool use_u_axis) { + std::array values = { + use_u_axis ? out[0].x() : out[0].y(), + use_u_axis ? out[1].x() : out[1].y(), + use_u_axis ? out[2].x() : out[2].y() + }; + if (!std::all_of(values.begin(), values.end(), [](float value) { return std::isfinite(value); })) + return; + auto span = [](const std::array &v) { + return std::max({ v[0], v[1], v[2] }) - std::min({ v[0], v[1], v[2] }); + }; + const bool has_repeat_evidence = std::any_of(values.begin(), values.end(), [](float value) { + constexpr float eps = 1e-6f; + return value < -eps || value > 1.f + eps; + }); + const float original_span = span(values); + if (!has_repeat_evidence || original_span <= 0.5f) + return; + std::array best = values; + float best_span = original_span; + for (size_t anchor = 0; anchor < values.size(); ++anchor) { + std::array candidate = values; + for (size_t i = 0; i < candidate.size(); ++i) { + const float delta = values[i] - values[anchor]; + candidate[i] = values[anchor] + delta - std::round(delta); + } + const float candidate_span = span(candidate); + if (candidate_span + 1e-6f < best_span) { + best = candidate; + best_span = candidate_span; + } + } + if (best_span >= original_span - 1e-6f) + return; + if (use_u_axis) { + out[0].x() = best[0]; + out[1].x() = best[1]; + out[2].x() = best[2]; + } else { + out[0].y() = best[0]; + out[1].y() = best[1]; + out[2].y() = best[2]; + } + }; + unwrap_axis(true); + unwrap_axis(false); + return out; +} + +template +bool accumulate_layer_plane_triangle_samples(const Vec3d &p0, + const Vec3d &p1, + const Vec3d &p2, + float layer_z_mm, + float layer_z_falloff_mm, + bool high_resolution_texture_sampling, + const SampleDataForBarycentricFn &sample_data_for_barycentric, + const AccumulateSampleFn &accumulate_sample) +{ + const float z0 = float(p0.z()); + const float z1 = float(p1.z()); + const float z2 = float(p2.z()); + if (!std::isfinite(z0) || !std::isfinite(z1) || !std::isfinite(z2)) + return false; + + const float min_z = std::min({ z0, z1, z2 }); + const float max_z = std::max({ z0, z1, z2 }); + const float z_eps = std::max(1e-5f, layer_z_falloff_mm * 1e-4f); + if (layer_z_mm < min_z - z_eps || layer_z_mm > max_z + z_eps || max_z - min_z <= z_eps) + return false; + + const std::array vertices = { p0, p1, p2 }; + const std::array barycentrics = { + Vec3f(1.f, 0.f, 0.f), + Vec3f(0.f, 1.f, 0.f), + Vec3f(0.f, 0.f, 1.f) + }; + const std::array zs = { z0, z1, z2 }; + std::vector layer_points; + layer_points.reserve(3); + + auto add_layer_point = [&layer_points](const Vec3d &p, const Vec3f &barycentric) { + if (!p.allFinite() || !barycentric.allFinite()) + return; + for (const LayerPlaneSamplePoint &existing : layer_points) + if ((existing.p - p).squaredNorm() <= 1e-10) + return; + layer_points.push_back({ p, barycentric }); + }; + + const std::array, 3> edges = { + std::make_pair(size_t(0), size_t(1)), + std::make_pair(size_t(1), size_t(2)), + std::make_pair(size_t(2), size_t(0)) + }; + for (const auto &edge : edges) { + const size_t a = edge.first; + const size_t b = edge.second; + const float da = zs[a] - layer_z_mm; + const float db = zs[b] - layer_z_mm; + const bool a_on_layer = std::abs(da) <= z_eps; + const bool b_on_layer = std::abs(db) <= z_eps; + if (a_on_layer) + add_layer_point(vertices[a], barycentrics[a]); + if (b_on_layer) + add_layer_point(vertices[b], barycentrics[b]); + if (a_on_layer || b_on_layer) + continue; + if ((da < 0.f && db > 0.f) || (da > 0.f && db < 0.f)) { + const float t = (layer_z_mm - zs[a]) / (zs[b] - zs[a]); + if (!std::isfinite(t) || t < -1e-4f || t > 1.f + 1e-4f) + continue; + const float clamped_t = std::clamp(t, 0.f, 1.f); + add_layer_point(vertices[a] * double(1.f - clamped_t) + vertices[b] * double(clamped_t), + barycentrics[a] * (1.f - clamped_t) + barycentrics[b] * clamped_t); + } + } + + if (layer_points.size() < 2) + return false; + + size_t best_a = 0; + size_t best_b = 1; + double best_length_sq = 0.0; + for (size_t i = 0; i + 1 < layer_points.size(); ++i) { + for (size_t j = i + 1; j < layer_points.size(); ++j) { + const double length_sq = (layer_points[i].p - layer_points[j].p).squaredNorm(); + if (length_sq > best_length_sq) { + best_a = i; + best_b = j; + best_length_sq = length_sq; + } + } + } + + const double segment_length_mm = std::sqrt(best_length_sq); + if (!std::isfinite(segment_length_mm) || segment_length_mm <= EPSILON) + return false; + + const float sample_pitch_mm = high_resolution_texture_sampling ? 0.08f : 0.16f; + const int sample_count = std::clamp(int(std::ceil(segment_length_mm / std::max(float(EPSILON), sample_pitch_mm))), 1, 2000); + const float sample_weight = std::max(0.05f, float(segment_length_mm) / float(sample_count)); + for (int sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const float t = (float(sample_idx) + 0.5f) / float(sample_count); + Vec3f barycentric = layer_points[best_a].barycentric * (1.f - t) + layer_points[best_b].barycentric * t; + barycentric.x() = std::max(0.f, barycentric.x()); + barycentric.y() = std::max(0.f, barycentric.y()); + barycentric.z() = std::max(0.f, barycentric.z()); + const float barycentric_sum = barycentric.x() + barycentric.y() + barycentric.z(); + if (!std::isfinite(barycentric_sum) || barycentric_sum <= EPSILON) + continue; + barycentric /= barycentric_sum; + + const Vec3d world_pos = p0 * double(barycentric.x()) + p1 * double(barycentric.y()) + p2 * double(barycentric.z()); + TextureSampleData sample_data = sample_data_for_barycentric(barycentric); + accumulate_sample(float(world_pos.x()), + float(world_pos.y()), + sample_data.rgba, + sample_weight, + std::move(sample_data.raw_component_weights), + sample_data.raw_component_weights_from_texture); + } + + return true; +} + +std::vector component_weights_for_sample(const WeightedTextureSample &sample, + size_t component_count, + bool raw_values_mode, + int filament_color_mode, + bool force_sequential_filaments, + int generic_solver_lookup_mode, + int generic_solver_mode, + bool use_fixed_color_generic_solver, + float contrast_factor, + float tone_gamma, + const std::vector> &component_colors, + const ColorSolverCandidateSet *generic_mix_candidates) +{ + std::vector desired(component_count, 0.f); + size_t mapped_component_count = component_count; + const bool has_raw_component_weights = sample.raw_component_weights.size() == component_count; + if (has_raw_component_weights) { + float raw_activity = 0.f; + for (size_t component_idx = 0; component_idx < component_count; ++component_idx) + desired[component_idx] = clamp01f(sample.raw_component_weights[component_idx]); + for (const float value : desired) + raw_activity = std::max(raw_activity, value); + if (raw_activity <= EPSILON && !sample.raw_component_weights_from_texture) + std::fill(desired.begin(), desired.end(), 1.f); + } else { + std::array target = { + clamp01f(sample.rgba[0]), + clamp01f(sample.rgba[1]), + clamp01f(sample.rgba[2]) + }; + if (std::abs(tone_gamma - 1.f) > 1e-5f) { + target[0] = apply_texture_tone_gamma(target[0], tone_gamma); + target[1] = apply_texture_tone_gamma(target[1], tone_gamma); + target[2] = apply_texture_tone_gamma(target[2], tone_gamma); + } + + if (raw_values_mode) { + const float channels[3] = { target[0], target[1], target[2] }; + const size_t channel_count = std::min(component_count, size_t(3)); + for (size_t channel_idx = 0; channel_idx < channel_count; ++channel_idx) + desired[channel_idx] = clamp01f(channels[channel_idx]); + mapped_component_count = channel_count; + } else { + std::vector optimized; + if (!use_fixed_color_generic_solver) + optimized = optimized_component_weights(target, + component_count, + filament_color_mode, + component_colors, + force_sequential_filaments); + if (optimized.size() == component_count) { + desired = std::move(optimized); + } else if (generic_mix_candidates != nullptr) { + std::vector best = + solve_color_solver_weights_for_target(*generic_mix_candidates, + target, + color_solver_lookup_mode_from_index(generic_solver_lookup_mode), + color_solver_mode_from_index(generic_solver_mode)); + if (best.size() == component_count) + desired = std::move(best); + } + } + } + + if (!has_raw_component_weights && std::abs(contrast_factor - 1.f) > 1e-5f) + apply_texture_contrast_to_components(desired, contrast_factor, mapped_component_count); + for (float &v : desired) + v = clamp01f(v); + return desired; +} + +TextureMappingOffsetWeightField build_texture_mapping_offset_weight_field( + const PrintObject &print_object, + const std::vector> &component_colors, + bool raw_values_mode, + int filament_color_mode, + bool force_sequential_filaments, + int generic_solver_lookup_mode, + int generic_solver_mode, + int generic_solver_mix_model, + bool use_legacy_fixed_color_mode, + float texture_contrast_pct, + float texture_tone_gamma, + float layer_z_mm, + float layer_z_falloff_mm, + bool high_resolution_texture_sampling) +{ + TextureMappingOffsetWeightField weight_field; + if (component_colors.empty()) + return weight_field; + + const size_t component_count = component_colors.size(); + const ModelObject *model_object = print_object.model_object(); + if (model_object == nullptr) + return weight_field; + + const BoundingBox object_bbox = print_object.bounding_box(); + const float min_x_mm = unscale(object_bbox.min.x()); + const float min_y_mm = unscale(object_bbox.min.y()); + const float max_x_mm = unscale(object_bbox.max.x()); + const float max_y_mm = unscale(object_bbox.max.y()); + const float span_x_mm = std::max(max_x_mm - min_x_mm, 1e-3f); + const float span_y_mm = std::max(max_y_mm - min_y_mm, 1e-3f); + if (!std::isfinite(min_x_mm) || !std::isfinite(min_y_mm) || + !std::isfinite(max_x_mm) || !std::isfinite(max_y_mm) || + !std::isfinite(span_x_mm) || !std::isfinite(span_y_mm)) + return {}; + + const float safe_layer_z_falloff_mm = std::max(layer_z_falloff_mm, 1e-3f); + const float contrast_factor = std::clamp(texture_contrast_pct, 25.f, 300.f) / 100.f; + const float tone_gamma = + (!std::isfinite(texture_tone_gamma) || texture_tone_gamma <= 0.f) ? 1.f : std::clamp(texture_tone_gamma, 0.5f, 3.f); + + std::vector samples; + samples.reserve(8192); + auto accumulate_sample = [&samples, component_count](float x_mm, + float y_mm, + const std::array &rgba, + float sample_weight, + std::vector raw_component_weights = {}, + bool raw_component_weights_from_texture = false) { + if (!std::isfinite(x_mm) || !std::isfinite(y_mm) || sample_weight <= EPSILON) + return; + if (!std::isfinite(sample_weight) || + !std::isfinite(rgba[0]) || + !std::isfinite(rgba[1]) || + !std::isfinite(rgba[2]) || + !std::isfinite(rgba[3])) + 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 }); + }; + + auto accumulate_constant_surface_triangle_samples = [&](const Vec3d &p0, + const Vec3d &p1, + const Vec3d &p2, + const std::array &rgba) { + if (accumulate_layer_plane_triangle_samples(p0, p1, p2, layer_z_mm, safe_layer_z_falloff_mm, high_resolution_texture_sampling, + [&rgba](const Vec3f &) { return TextureSampleData{ rgba, {}, false }; }, accumulate_sample)) + return; + + const float min_z = std::min({ float(p0.z()), float(p1.z()), float(p2.z()) }); + const float max_z = std::max({ float(p0.z()), float(p1.z()), float(p2.z()) }); + if (layer_z_mm < min_z - 4.f * safe_layer_z_falloff_mm || layer_z_mm > max_z + 4.f * safe_layer_z_falloff_mm) + return; + + const float max_world_edge_mm = std::max({ float((p1 - p0).norm()), float((p2 - p1).norm()), float((p0 - p2).norm()) }); + if (!std::isfinite(max_world_edge_mm)) + return; + const double tri_area_mm2 = 0.5 * ((p1 - p0).cross(p2 - p0)).norm(); + if (!std::isfinite(tri_area_mm2)) + return; + + const float pitch_mm = high_resolution_texture_sampling ? 0.08f : 0.16f; + const int max_bary_steps = high_resolution_texture_sampling ? 80 : 40; + const int bary_steps = std::clamp(int(std::ceil(max_world_edge_mm / pitch_mm)), 1, max_bary_steps); + const int sample_count = bary_steps * (bary_steps + 1) / 2; + if (sample_count <= 0) + return; + const float area_weight = std::max(0.05f, float(tri_area_mm2)) / float(sample_count); + const float inv_steps = 1.f / float(bary_steps); + for (int i = 0; i < bary_steps; ++i) { + for (int j = 0; j < (bary_steps - i); ++j) { + const float b1 = (float(i) + 0.33333334f) * inv_steps; + const float b2 = (float(j) + 0.33333334f) * inv_steps; + const float b0 = 1.f - b1 - b2; + if (b0 < 0.f) + continue; + const Vec3d world_pos = p0 * double(b0) + p1 * double(b1) + p2 * double(b2); + const float dz = std::abs(float(world_pos.z()) - layer_z_mm); + const float z_norm = dz / safe_layer_z_falloff_mm; + 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); + } + } + }; + + const Transform3d object_trafo = print_object.trafo_centered(); + for (const ModelVolume *volume : model_object->volumes) { + if (volume == nullptr) + continue; + + const std::shared_ptr mesh_ptr = volume->mesh_ptr(); + if (!mesh_ptr) + continue; + + const indexed_triangle_set &its = mesh_ptr->its; + const Transform3d volume_trafo = object_trafo * volume->get_matrix(); + const std::array background_color = texture_mapping_background_color(*volume); + + if (!volume->texture_mapping_color_facets.empty()) { + std::vector color_facets; + volume->texture_mapping_color_facets.get_facet_triangles(*volume, color_facets); + std::vector rgba_source_triangles(its.indices.size(), 0); + for (const ColorFacetTriangle &facet : color_facets) { + if (facet.source_triangle >= 0 && size_t(facet.source_triangle) < rgba_source_triangles.size()) + rgba_source_triangles[size_t(facet.source_triangle)] = 1; + + const Vec3d p0 = volume_trafo * facet.vertices[0].cast(); + const Vec3d p1 = volume_trafo * facet.vertices[1].cast(); + const Vec3d p2 = volume_trafo * facet.vertices[2].cast(); + if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) + continue; + const std::array rgba = composite_rgba_over_background(unpack_rgba_u32(facet.rgba), background_color); + accumulate_constant_surface_triangle_samples(p0, p1, p2, rgba); + } + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + if (tri_idx < rgba_source_triangles.size() && rgba_source_triangles[tri_idx] != 0) + continue; + + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + continue; + if (size_t(tri[0]) >= its.vertices.size() || + size_t(tri[1]) >= its.vertices.size() || + size_t(tri[2]) >= its.vertices.size()) + continue; + + const Vec3d p0 = volume_trafo * its.vertices[size_t(tri[0])].cast(); + const Vec3d p1 = volume_trafo * its.vertices[size_t(tri[1])].cast(); + const Vec3d p2 = volume_trafo * its.vertices[size_t(tri[2])].cast(); + if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) + continue; + + accumulate_constant_surface_triangle_samples(p0, p1, p2, background_color); + } + continue; + } + + const bool has_uv_texture = + !volume->imported_texture_rgba.empty() && + volume->imported_texture_width > 0 && + volume->imported_texture_height > 0 && + volume->imported_texture_uv_valid.size() == its.indices.size() && + volume->imported_texture_uvs_per_face.size() >= its.indices.size() * 6 && + volume->imported_texture_rgba.size() >= size_t(volume->imported_texture_width) * size_t(volume->imported_texture_height) * 4; + if (has_uv_texture) { + const std::vector raw_component_channels = + raw_component_source_channels(volume->imported_texture_raw_metadata_json, + volume->imported_texture_raw_channels, + filament_color_mode, + component_count, + component_colors); + const bool use_raw_uv_texture = + raw_component_channels.size() == component_count && + volume->imported_texture_raw_filament_offsets.size() >= + size_t(volume->imported_texture_width) * + size_t(volume->imported_texture_height) * + size_t(volume->imported_texture_raw_channels); + auto sample_data_for_uv = [&](const Vec2f &uv) { + std::array rgba = + sample_texture_rgba_bilinear(volume->imported_texture_rgba, + volume->imported_texture_width, + volume->imported_texture_height, + uv.x(), + uv.y()); + std::vector raw_component_weights; + if (use_raw_uv_texture) { + const std::vector raw_sample = + sample_texture_raw_offsets_bilinear(volume->imported_texture_raw_filament_offsets, + volume->imported_texture_width, + volume->imported_texture_height, + volume->imported_texture_raw_channels, + uv.x(), + uv.y()); + raw_component_weights = map_raw_sample_to_components(raw_sample, raw_component_channels); + if (raw_component_weights.size() == component_count) + rgba = raw_offset_preview_rgba(raw_component_weights); + } + if (raw_component_weights.size() != component_count) + rgba = composite_rgba_over_background(rgba, background_color); + return TextureSampleData{ rgba, std::move(raw_component_weights), use_raw_uv_texture }; + }; + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + if (volume->imported_texture_uv_valid[tri_idx] == 0) + continue; + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + continue; + if (size_t(tri[0]) >= its.vertices.size() || + size_t(tri[1]) >= its.vertices.size() || + size_t(tri[2]) >= its.vertices.size()) + continue; + const Vec3d p0 = volume_trafo * its.vertices[size_t(tri[0])].cast(); + const Vec3d p1 = volume_trafo * its.vertices[size_t(tri[1])].cast(); + const Vec3d p2 = volume_trafo * its.vertices[size_t(tri[2])].cast(); + if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) + continue; + + const size_t uv_off = tri_idx * 6; + const std::array uvs = unwrap_triangle_uvs( + Vec2f(volume->imported_texture_uvs_per_face[uv_off + 0], volume->imported_texture_uvs_per_face[uv_off + 1]), + Vec2f(volume->imported_texture_uvs_per_face[uv_off + 2], volume->imported_texture_uvs_per_face[uv_off + 3]), + Vec2f(volume->imported_texture_uvs_per_face[uv_off + 4], volume->imported_texture_uvs_per_face[uv_off + 5])); + + if (accumulate_layer_plane_triangle_samples(p0, p1, p2, layer_z_mm, safe_layer_z_falloff_mm, high_resolution_texture_sampling, + [&uvs, &sample_data_for_uv](const Vec3f &barycentric) { + const Vec2f uv = uvs[0] * barycentric.x() + uvs[1] * barycentric.y() + uvs[2] * barycentric.z(); + return sample_data_for_uv(uv); + }, accumulate_sample)) + continue; + + const float min_z = std::min({ float(p0.z()), float(p1.z()), float(p2.z()) }); + const float max_z = std::max({ float(p0.z()), float(p1.z()), float(p2.z()) }); + if (layer_z_mm < min_z - 4.f * safe_layer_z_falloff_mm || layer_z_mm > max_z + 4.f * safe_layer_z_falloff_mm) + continue; + + const float max_world_edge_mm = std::max({ float((p1 - p0).norm()), float((p2 - p1).norm()), float((p0 - p2).norm()) }); + const double tri_area_mm2 = 0.5 * ((p1 - p0).cross(p2 - p0)).norm(); + if (!std::isfinite(max_world_edge_mm) || !std::isfinite(tri_area_mm2)) + continue; + const float pitch_mm = high_resolution_texture_sampling ? 0.08f : 0.16f; + const int max_bary_steps = high_resolution_texture_sampling ? 80 : 40; + const int bary_steps = std::clamp(int(std::ceil(max_world_edge_mm / pitch_mm)), 1, max_bary_steps); + const int sample_count = bary_steps * (bary_steps + 1) / 2; + if (sample_count <= 0) + continue; + const float area_weight = std::max(0.05f, float(tri_area_mm2)) / float(sample_count); + const float inv_steps = 1.f / float(bary_steps); + for (int i = 0; i < bary_steps; ++i) { + for (int j = 0; j < (bary_steps - i); ++j) { + const float b1 = (float(i) + 0.33333334f) * inv_steps; + const float b2 = (float(j) + 0.33333334f) * inv_steps; + const float b0 = 1.f - b1 - b2; + if (b0 < 0.f) + continue; + const Vec3d world_pos = p0 * double(b0) + p1 * double(b1) + p2 * double(b2); + const Vec2f uv = uvs[0] * b0 + uvs[1] * b1 + uvs[2] * b2; + TextureSampleData sample_data = sample_data_for_uv(uv); + const float dz = std::abs(float(world_pos.z()) - layer_z_mm); + const float z_norm = dz / safe_layer_z_falloff_mm; + 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()), + sample_data.rgba, + area_weight * z_weight, + std::move(sample_data.raw_component_weights), + sample_data.raw_component_weights_from_texture); + } + } + } + continue; + } + + if (volume->imported_vertex_colors_rgba.empty() || its.vertices.size() != volume->imported_vertex_colors_rgba.size()) + continue; + for (size_t i = 0; i < its.vertices.size(); ++i) { + const Vec3d world_pos = volume_trafo * its.vertices[i].cast(); + if (!world_pos.allFinite()) + continue; + const float dz = std::abs(float(world_pos.z()) - layer_z_mm); + if (dz > 4.f * safe_layer_z_falloff_mm) + continue; + const float z_norm = dz / safe_layer_z_falloff_mm; + const float sample_weight = std::exp(-0.5f * z_norm * z_norm); + if (!std::isfinite(sample_weight) || sample_weight <= EPSILON) + continue; + const std::array rgba = + composite_rgba_over_background(unpack_rgba_u32(volume->imported_vertex_colors_rgba[i]), background_color); + accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, sample_weight); + } + } + + if (samples.empty()) + return TextureMappingOffsetWeightField{}; + + const std::vector> fixed_colors = fixed_color_generic_solver_component_colors(filament_color_mode); + const bool use_fixed_color_generic_solver = + !raw_values_mode && + !use_legacy_fixed_color_mode && + fixed_colors.size() == component_count; + const std::vector> &solver_colors = + use_fixed_color_generic_solver ? fixed_colors : component_colors; + ColorSolverCandidateSet candidates; + const ColorSolverCandidateSet *candidate_ptr = nullptr; + if (!raw_values_mode) { + candidates = build_color_solver_candidates(solver_colors, color_solver_mix_model_from_index(generic_solver_mix_model)); + candidate_ptr = candidates.empty() ? nullptr : &candidates; + } + + const size_t sample_count = samples.size(); + weight_field.component_count = component_count; + weight_field.sample_x_mm.resize(sample_count); + weight_field.sample_y_mm.resize(sample_count); + weight_field.sample_weight.resize(sample_count); + weight_field.sample_component_weights.assign(sample_count * component_count, 0.f); + weight_field.raw_component_weights_from_texture = false; + + std::vector fallback_acc(component_count, 0.f); + float fallback_weight = 0.f; + for (size_t sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const WeightedTextureSample &sample = samples[sample_idx]; + if (sample.weight <= EPSILON) + continue; + if (sample.raw_component_weights_from_texture) + weight_field.raw_component_weights_from_texture = true; + weight_field.sample_x_mm[sample_idx] = sample.x_mm; + weight_field.sample_y_mm[sample_idx] = sample.y_mm; + weight_field.sample_weight[sample_idx] = sample.weight; + + std::vector desired = + component_weights_for_sample(sample, + component_count, + raw_values_mode, + filament_color_mode, + force_sequential_filaments, + generic_solver_lookup_mode, + generic_solver_mode, + use_fixed_color_generic_solver, + contrast_factor, + tone_gamma, + component_colors, + candidate_ptr); + for (size_t component_idx = 0; component_idx < component_count; ++component_idx) { + const float v = component_idx < desired.size() ? clamp01f(desired[component_idx]) : 0.f; + weight_field.sample_component_weights[sample_idx * component_count + component_idx] = v; + fallback_acc[component_idx] += v * sample.weight; + } + fallback_weight += sample.weight; + } + + weight_field.fallback_weights.assign(component_count, 1.f / float(component_count)); + if (fallback_weight > EPSILON) { + for (size_t component_idx = 0; component_idx < component_count; ++component_idx) + weight_field.fallback_weights[component_idx] = clamp01f(fallback_acc[component_idx] / fallback_weight); + } + + const float target_bucket_mm = high_resolution_texture_sampling ? 0.12f : 0.22f; + constexpr int min_bucket_dim = 16; + constexpr int max_bucket_dim = 320; + constexpr int max_buckets = 72000; + int bucket_width = std::clamp(int(std::ceil(span_x_mm / target_bucket_mm)) + 1, min_bucket_dim, max_bucket_dim); + int bucket_height = std::clamp(int(std::ceil(span_y_mm / target_bucket_mm)) + 1, min_bucket_dim, max_bucket_dim); + const int initial_buckets = bucket_width * bucket_height; + if (initial_buckets > max_buckets) { + const float scale_factor = std::sqrt(float(initial_buckets) / float(max_buckets)); + bucket_width = std::max(min_bucket_dim, int(std::ceil(float(bucket_width) / scale_factor))); + bucket_height = std::max(min_bucket_dim, int(std::ceil(float(bucket_height) / scale_factor))); + } + + weight_field.min_x_mm = min_x_mm; + weight_field.min_y_mm = min_y_mm; + weight_field.bucket_width = bucket_width; + weight_field.bucket_height = bucket_height; + weight_field.bucket_width_mm = std::max(1e-3f, span_x_mm / std::max(1, bucket_width - 1)); + weight_field.bucket_height_mm = std::max(1e-3f, span_y_mm / std::max(1, bucket_height - 1)); + weight_field.buckets.assign(size_t(bucket_width) * size_t(bucket_height), {}); + for (size_t sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const float gx = (weight_field.sample_x_mm[sample_idx] - min_x_mm) / weight_field.bucket_width_mm; + const float gy = (weight_field.sample_y_mm[sample_idx] - min_y_mm) / weight_field.bucket_height_mm; + const int bx = std::clamp(int(std::floor(gx)), 0, bucket_width - 1); + const int by = std::clamp(int(std::floor(gy)), 0, bucket_height - 1); + weight_field.buckets[size_t(by) * size_t(bucket_width) + size_t(bx)].push_back(uint32_t(sample_idx)); + } + return weight_field; +} + +std::vector sample_weight_field_components(const TextureMappingOffsetWeightField &weight_field, + float x_mm, + float y_mm, + bool high_resolution_texture_sampling) +{ + std::vector fallback = weight_field.fallback_weights; + if (fallback.size() < weight_field.component_count) + fallback.resize(weight_field.component_count, 0.f); + if (weight_field.empty() || !std::isfinite(x_mm) || !std::isfinite(y_mm)) + return fallback; + + 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 sigma_scale = high_resolution_texture_sampling ? 0.45f : 0.7f; + const float min_sigma_mm = high_resolution_texture_sampling ? 0.04f : 0.06f; + const float sigma_x_mm = std::max(min_sigma_mm, weight_field.bucket_width_mm * sigma_scale); + const float sigma_y_mm = std::max(min_sigma_mm, weight_field.bucket_height_mm * sigma_scale); + const float inv_two_sigma_x2 = 1.f / std::max(2.f * sigma_x_mm * sigma_x_mm, 1e-8f); + const float inv_two_sigma_y2 = 1.f / std::max(2.f * sigma_y_mm * sigma_y_mm, 1e-8f); + const float min_radius_mm = high_resolution_texture_sampling ? 0.16f : 0.30f; + const float radius_scale = high_resolution_texture_sampling ? 1.75f : 3.f; + const float max_radius_mm = std::max(min_radius_mm, std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * radius_scale); + const float max_radius2 = max_radius_mm * max_radius_mm; + const float min_bucket_span_mm = std::max(1e-3f, std::min(weight_field.bucket_width_mm, weight_field.bucket_height_mm)); + const int max_ring = std::max(1, int(std::ceil(max_radius_mm / min_bucket_span_mm))); + + std::vector weighted_sum(weight_field.component_count, 0.f); + float total_weight = 0.f; + size_t contributing_samples = 0; + auto process_bucket = [&](int bx, int by) { + if (bx < 0 || by < 0 || bx >= weight_field.bucket_width || by >= weight_field.bucket_height) + return; + const size_t bucket_idx = size_t(by) * size_t(weight_field.bucket_width) + size_t(bx); + if (bucket_idx >= weight_field.buckets.size()) + return; + 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_y_mm.size() || + sample_idx >= weight_field.sample_weight.size()) + 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 > max_radius2) + continue; + const float kernel = std::exp(-(dx * dx) * inv_two_sigma_x2 - (dy * dy) * inv_two_sigma_y2); + const float sample_w = weight_field.sample_weight[sample_idx] * kernel; + if (!std::isfinite(sample_w) || sample_w <= EPSILON) + continue; + const size_t value_idx = sample_idx * weight_field.component_count; + if (value_idx + weight_field.component_count > weight_field.sample_component_weights.size()) + continue; + for (size_t component_idx = 0; component_idx < weight_field.component_count; ++component_idx) + weighted_sum[component_idx] += weight_field.sample_component_weights[value_idx + component_idx] * sample_w; + total_weight += sample_w; + ++contributing_samples; + } + }; + + 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); + if (ring == 0) { + process_bucket(cx, cy); + } else { + for (int x = min_x; x <= max_x; ++x) { + process_bucket(x, min_y); + if (max_y != min_y) + process_bucket(x, max_y); + } + for (int y = min_y + 1; y <= max_y - 1; ++y) { + process_bucket(min_x, y); + if (max_x != min_x) + process_bucket(max_x, y); + } + } + if (total_weight > EPSILON && contributing_samples >= 12) + break; + } + + if (total_weight > EPSILON) { + std::vector values(weight_field.component_count, 0.f); + for (size_t component_idx = 0; component_idx < weight_field.component_count; ++component_idx) + values[component_idx] = clamp01f(weighted_sum[component_idx] / total_weight); + return values; + } + return fallback; +} + +float sample_weight_field(const TextureMappingOffsetWeightField &weight_field, + float x_mm, + float y_mm, + size_t component_idx, + bool high_resolution_texture_sampling, + bool compact_offset_mode) +{ + const float fallback = component_idx < weight_field.fallback_weights.size() ? + weight_field.fallback_weights[component_idx] : 0.f; + if (weight_field.empty() || component_idx >= weight_field.component_count) + return fallback; + + std::vector values = sample_weight_field_components(weight_field, x_mm, y_mm, high_resolution_texture_sampling); + if (component_idx >= values.size()) + return fallback; + if (compact_offset_mode && !weight_field.raw_component_weights_from_texture) { + float max_value = 0.f; + for (size_t idx = 0; idx < weight_field.component_count && idx < values.size(); ++idx) + max_value = std::max(max_value, clamp01f(values[idx])); + if (max_value > EPSILON) + return clamp01f(values[component_idx] / max_value); + } + return clamp01f(values[component_idx]); +} + +bool texture_mapping_component_is_black(size_t component_idx, + int filament_color_mode, + const std::vector> &component_colors) +{ + switch (std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorRGBKW))) { + case int(TextureMappingZone::FilamentColorCMYK): + case int(TextureMappingZone::FilamentColorRGBK): + case int(TextureMappingZone::FilamentColorCMYKW): + case int(TextureMappingZone::FilamentColorRGBKW): + if (component_idx == 3) + return true; + break; + case int(TextureMappingZone::FilamentColorBW): + if (component_idx == 0) + return true; + break; + default: + break; + } + if (component_idx >= component_colors.size()) + return false; + const std::array &c = component_colors[component_idx]; + const float max_channel = std::max({ c[0], c[1], c[2] }); + const float luminance = 0.2126f * c[0] + 0.7152f * c[1] + 0.0722f * c[2]; + return max_channel <= 0.18f && luminance <= 0.12f; +} + +bool explicit_transmission_distance(const TextureMappingZone &zone, unsigned int physical_filament_id, float &td_mm) +{ + if (physical_filament_id == 0) + return false; + const size_t idx = size_t(physical_filament_id - 1); + if (idx >= zone.filament_transmission_distances_mm.size()) + return false; + const float value = zone.filament_transmission_distances_mm[idx]; + if (!std::isfinite(value) || value <= 0.f) + return false; + td_mm = std::clamp(value, 0.01f, 50.f); + return true; +} + +float transmission_distance_reference(bool is_black) +{ + return is_black ? 0.1f : 3.f; +} + +float transmission_distance_opacity(float td_mm, float path_extension_mm) +{ + constexpr float surface_scatter = 0.50f; + constexpr float surface_depth_mm = 0.32f; + const float safe_td = std::clamp(td_mm, 0.01f, 50.f); + const float path_mm = std::max(0.f, surface_depth_mm + std::max(0.f, path_extension_mm)); + const float opacity = surface_scatter + (1.f - surface_scatter) * (1.f - std::exp(-path_mm / safe_td)); + return std::clamp(opacity, 1e-4f, 1.f); +} + +TransmissionDistanceCalibrationContext transmission_distance_calibration_context( + const TextureMappingZone &zone, + const std::vector &component_ids, + const std::vector> &component_colors, + int filament_color_mode) +{ + TransmissionDistanceCalibrationContext context; + context.mode = std::clamp(zone.transmission_distance_calibration_mode, + int(TextureMappingZone::TDCalibrationNone), + int(TextureMappingZone::TDCalibrationNeighbor)); + if (context.mode == int(TextureMappingZone::TDCalibrationNone) || component_ids.empty()) + return context; + + std::vector explicit_tds(component_ids.size(), 0.f); + bool has_active_explicit_td = false; + for (size_t idx = 0; idx < component_ids.size(); ++idx) { + float td_mm = 0.f; + if (explicit_transmission_distance(zone, component_ids[idx], td_mm)) { + explicit_tds[idx] = td_mm; + has_active_explicit_td = true; + } + } + if (!has_active_explicit_td) + return context; + + const bool neighbor_mode = context.mode == int(TextureMappingZone::TDCalibrationNeighbor); + const float path_extension_mm = neighbor_mode ? 0.16f : 0.12f; + const float own_power = neighbor_mode ? 0.25f : 0.35f; + context.own_width_factors.assign(component_ids.size(), 1.f); + context.neighbor_opacity_ratios.assign(component_ids.size(), 1.f); + context.enabled = true; + for (size_t idx = 0; idx < component_ids.size(); ++idx) { + const bool is_black = texture_mapping_component_is_black(idx, filament_color_mode, component_colors); + const float reference_td_mm = transmission_distance_reference(is_black); + const float actual_td_mm = explicit_tds[idx] > 0.f ? explicit_tds[idx] : reference_td_mm; + const float actual_opacity = transmission_distance_opacity(actual_td_mm, path_extension_mm); + const float reference_opacity = transmission_distance_opacity(reference_td_mm, path_extension_mm); + context.own_width_factors[idx] = + std::clamp(std::pow(reference_opacity / std::max(actual_opacity, 1e-4f), own_power), 0.25f, 2.f); + context.neighbor_opacity_ratios[idx] = + std::clamp(actual_opacity / std::max(reference_opacity, 1e-4f), 0.25f, 4.f); + } + return context; +} + +float transmission_distance_width_factor(const TransmissionDistanceCalibrationContext &context, + size_t active_component_idx, + size_t previous_component_idx) +{ + if (!context.enabled || active_component_idx >= context.own_width_factors.size()) + return 1.f; + + float factor = context.own_width_factors[active_component_idx]; + if (context.mode == int(TextureMappingZone::TDCalibrationNeighbor) && + previous_component_idx < context.neighbor_opacity_ratios.size()) + factor *= std::pow(context.neighbor_opacity_ratios[previous_component_idx], 0.20f); + return std::clamp(factor, 0.25f, 2.f); +} + +float nonlinear_visibility_width_factor(float desired_width_factor, + float layer_height_mm, + float stair_step_mm, + float max_width_delta_limit_mm) +{ + const float r = clamp01f(desired_width_factor); + if (!std::isfinite(layer_height_mm) || + !std::isfinite(stair_step_mm) || + !std::isfinite(max_width_delta_limit_mm) || + layer_height_mm <= EPSILON || + max_width_delta_limit_mm <= EPSILON) + return r; + if (r <= EPSILON || r >= 1.f - EPSILON) + return r; + if (std::abs(r - 0.5f) <= 1e-5f) + return 0.5f; + + const float h = std::max(0.01f, layer_height_mm); + const float d = std::max(0.f, stair_step_mm); + const float diag = std::hypot(h, d); + if (!std::isfinite(diag) || diag <= EPSILON) + return r; + + const float symmetric_r = std::min(r, 1.f - r); + const float direction = r >= 0.5f ? 1.f : -1.f; + const float sin_n = std::clamp(d / diag, 0.f, 1.f); + const float cos_n = std::clamp(h / diag, 1e-4f, 1.f); + const float sin_cos = sin_n * cos_n; + float offset_mm = 0.f; + if (sin_cos > 1e-5f) { + offset_mm = (0.5f - symmetric_r) * h / sin_cos; + if (2.f * std::abs(offset_mm) <= d + EPSILON) + return std::clamp(0.5f + direction * offset_mm / max_width_delta_limit_mm, 0.f, 1.f); + } + + const float cx = std::clamp(1.f - std::sqrt(2.f) / 2.f, 0.f, 0.95f); + const float c = (1.f - cx) * (1.f - cx); + const float safe_cos = std::max(cos_n, 1e-4f); + const float tan_n = sin_n / safe_cos; + const float a = -0.5f * c * (1.f + sin_n) / std::max(h * diag, 1e-6f); + const float b = 0.5f * (c * tan_n * (1.f + sin_n) + 2.f * cos_n * (cx - 1.f)) / std::max(diag, 1e-6f); + const float q = c * 0.25f * tan_n * (1.f + sin_n) - cx * cos_n; + const float cc = 0.5f - 0.5f * cos_n * q - symmetric_r; + const float det = std::max(0.f, b * b - 4.f * a * cc); + if (std::abs(a) > 1e-8f) { + offset_mm = (-b - std::sqrt(det)) / (2.f * a); + if (!std::isfinite(offset_mm) || offset_mm < 0.f) + offset_mm = (-b + std::sqrt(det)) / (2.f * a); + } + if (!std::isfinite(offset_mm) || offset_mm < 0.f) { + if (sin_cos > 1e-5f) + offset_mm = (0.5f - symmetric_r) * h / sin_cos; + else + offset_mm = max_width_delta_limit_mm; + } + return std::clamp(0.5f + direction * offset_mm / max_width_delta_limit_mm, 0.f, 1.f); +} + +float variable_width_delta_for_visibility_range(float inset_strength, + float max_width_delta_limit_mm, + float minimum_offset_factor, + float strength_factor, + float td_width_factor, + bool nonlinear_offset_adjustment, + float layer_height_mm, + float stair_step_mm) +{ + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= 0.f) + return 0.f; + float desired_width_factor = + std::clamp((1.f - std::clamp(inset_strength, 0.f, 1.f)) * + std::clamp(td_width_factor, 0.f, 2.f), + 0.f, + 1.f); + if (nonlinear_offset_adjustment) + desired_width_factor = nonlinear_visibility_width_factor(desired_width_factor, + layer_height_mm, + stair_step_mm, + max_width_delta_limit_mm); + const float min_width_factor = std::clamp(minimum_offset_factor, 0.f, 1.f); + const float adjusted_width_factor = + min_width_factor + desired_width_factor * std::clamp(strength_factor, 0.f, 1.f) * (1.f - min_width_factor); + return std::clamp(max_width_delta_limit_mm * (1.f - adjusted_width_factor), 0.f, max_width_delta_limit_mm); +} + +double bbox_distance_sq_to_point(const BoundingBox &bbox, const Point &point) +{ + if (!bbox.defined) + return 0.0; + + double dx = 0.0; + if (point.x() < bbox.min.x()) + dx = double(bbox.min.x() - point.x()); + else if (point.x() > bbox.max.x()) + dx = double(point.x() - bbox.max.x()); + + double dy = 0.0; + if (point.y() < bbox.min.y()) + dy = double(bbox.min.y() - point.y()); + else if (point.y() > bbox.max.y()) + dy = double(point.y() - bbox.max.y()); + return dx * dx + dy * dy; +} + +bool find_nearest_layer_slice_boundary_point(const Layer *layer, const Point &query_point, Point &nearest_point) +{ + if (layer == nullptr || layer->lslices.empty()) + return false; + + const bool has_slice_bboxes = layer->lslices_bboxes.size() == layer->lslices.size(); + double best_distance_sq = std::numeric_limits::max(); + bool found = false; + for (size_t slice_idx = 0; slice_idx < layer->lslices.size(); ++slice_idx) { + const ExPolygon &slice = layer->lslices[slice_idx]; + if (slice.empty()) + continue; + if (has_slice_bboxes && layer->lslices_bboxes[slice_idx].defined) { + const double bbox_distance_sq = bbox_distance_sq_to_point(layer->lslices_bboxes[slice_idx], query_point); + if (bbox_distance_sq > best_distance_sq) + continue; + } + const Point projected = slice.point_projection(query_point); + const double projected_distance_sq = (projected - query_point).cast().squaredNorm(); + if (projected_distance_sq < best_distance_sq) { + best_distance_sq = projected_distance_sq; + nearest_point = projected; + found = true; + } + } + return found; +} + +float local_surface_stair_step_distance(const Layer *layer, + const Point &mid_point, + double outward_x, + double outward_y, + float base_outer_width_mm, + float max_allowed_distance_mm) +{ + if (layer == nullptr || !std::isfinite(outward_x) || !std::isfinite(outward_y)) + return std::numeric_limits::quiet_NaN(); + + const double half_width_scaled = scale_(0.5 * double(std::max(0.01f, base_outer_width_mm))); + const Point current_base_edge( + coord_t(std::llround(double(mid_point.x()) + outward_x * half_width_scaled)), + coord_t(std::llround(double(mid_point.y()) + outward_y * half_width_scaled))); + const float max_local_edge_tangent_delta_mm = std::max(1.0f, base_outer_width_mm * 2.f); + const float max_local_edge_normal_delta_mm = + std::max(2.0f, base_outer_width_mm * 4.f + 2.f * std::max(0.f, max_allowed_distance_mm)); + float best_distance_mm = std::numeric_limits::quiet_NaN(); + + auto consider_adjacent_layer = [&](const Layer *adjacent_layer) { + if (adjacent_layer == nullptr) + return; + Point adjacent_base_edge; + if (!find_nearest_layer_slice_boundary_point(adjacent_layer, current_base_edge, adjacent_base_edge)) + return; + const double edge_delta_x = double(adjacent_base_edge.x()) - double(current_base_edge.x()); + const double edge_delta_y = double(adjacent_base_edge.y()) - double(current_base_edge.y()); + const double edge_distance_scaled = std::hypot(edge_delta_x, edge_delta_y); + const double edge_normal_delta_scaled = edge_delta_x * outward_x + edge_delta_y * outward_y; + const double edge_tangent_delta_scaled_sq = + std::max(0.0, edge_distance_scaled * edge_distance_scaled - edge_normal_delta_scaled * edge_normal_delta_scaled); + const float edge_tangent_delta_mm = unscale(std::sqrt(edge_tangent_delta_scaled_sq)); + if (!std::isfinite(edge_tangent_delta_mm) || edge_tangent_delta_mm > max_local_edge_tangent_delta_mm) + return; + const float edge_normal_delta_mm = std::abs(unscale(edge_normal_delta_scaled)); + if (!std::isfinite(edge_normal_delta_mm) || edge_normal_delta_mm > max_local_edge_normal_delta_mm) + return; + if (!std::isfinite(best_distance_mm) || edge_normal_delta_mm < best_distance_mm) + best_distance_mm = edge_normal_delta_mm; + }; + + consider_adjacent_layer(layer->upper_layer); + consider_adjacent_layer(layer->lower_layer); + return best_distance_mm; +} + +float component_angular_influence(unsigned int active_component_id, + float theta_deg, + const std::vector &component_ids, + const std::vector &component_angles_deg) +{ + if (component_ids.empty() || component_ids.size() != component_angles_deg.size()) + return 0.f; + const auto active_it = std::find(component_ids.begin(), component_ids.end(), active_component_id); + if (active_it == component_ids.end()) + return 0.f; + if (component_ids.size() == 1) + return 1.f; + + struct SortedComponentAngle { + float angle_deg { 0.f }; + size_t component_idx { 0 }; + }; + std::vector sorted_angles; + sorted_angles.reserve(component_ids.size()); + for (size_t i = 0; i < component_ids.size(); ++i) + sorted_angles.push_back({ normalize_texture_mapping_offset_angle_deg(component_angles_deg[i]), i }); + std::sort(sorted_angles.begin(), sorted_angles.end(), [](const SortedComponentAngle &lhs, const SortedComponentAngle &rhs) { + return lhs.angle_deg < rhs.angle_deg; + }); + + const size_t active_component_idx = size_t(active_it - component_ids.begin()); + const auto sorted_active_it = std::find_if(sorted_angles.begin(), sorted_angles.end(), + [active_component_idx](const SortedComponentAngle &entry) { + return entry.component_idx == active_component_idx; + }); + if (sorted_active_it == sorted_angles.end()) + return 0.f; + + const size_t sorted_pos = size_t(sorted_active_it - sorted_angles.begin()); + const size_t count = sorted_angles.size(); + const float prev_angle = sorted_angles[(sorted_pos + count - 1) % count].angle_deg; + const float self_angle = sorted_angles[sorted_pos].angle_deg; + const float next_angle = sorted_angles[(sorted_pos + 1) % count].angle_deg; + const float prev_to_self_deg = normalize_texture_mapping_offset_angle_deg(self_angle - prev_angle); + const float self_to_next_deg = normalize_texture_mapping_offset_angle_deg(next_angle - self_angle); + if (prev_to_self_deg <= 1e-3f || self_to_next_deg <= 1e-3f) { + float total_weight = 0.f; + float active_weight = 0.f; + for (size_t i = 0; i < component_ids.size(); ++i) { + const float dist = std::abs(normalize_texture_mapping_offset_angle_deg(theta_deg) - + normalize_texture_mapping_offset_angle_deg(component_angles_deg[i])); + const float wrapped_dist = std::min(dist, 360.f - dist); + const float weight = std::max(0.f, 1.f - wrapped_dist / 180.f); + total_weight += weight; + if (component_ids[i] == active_component_id) + active_weight += weight; + } + return total_weight <= EPSILON ? 0.f : std::clamp(active_weight / total_weight, 0.f, 1.f); + } + + const float theta_norm = normalize_texture_mapping_offset_angle_deg(theta_deg); + const float prev_to_theta_deg = normalize_texture_mapping_offset_angle_deg(theta_norm - prev_angle); + if (prev_to_theta_deg <= prev_to_self_deg + 1e-4f) + return std::clamp(prev_to_theta_deg / prev_to_self_deg, 0.f, 1.f); + const float self_to_theta_deg = normalize_texture_mapping_offset_angle_deg(theta_norm - self_angle); + if (self_to_theta_deg <= self_to_next_deg + 1e-4f) + return std::clamp(1.f - self_to_theta_deg / self_to_next_deg, 0.f, 1.f); + return 0.f; +} + +} + +std::vector decode_texture_mapping_offset_component_ids(const TextureMappingZone &zone, size_t num_physical) +{ + std::vector out; + for (const char c : zone.component_ids) { + if (c < '1' || c > '9') + continue; + const unsigned int id = unsigned(c - '0'); + if (id == 0 || id > num_physical) + continue; + if (std::find(out.begin(), out.end(), id) == out.end()) + out.emplace_back(id); + } + if (out.empty()) { + if (zone.component_a >= 1 && zone.component_a <= num_physical) + out.emplace_back(zone.component_a); + if (zone.component_b >= 1 && zone.component_b <= num_physical && + std::find(out.begin(), out.end(), zone.component_b) == out.end()) + out.emplace_back(zone.component_b); + } + return out; +} + +float normalize_texture_mapping_offset_angle_deg(float angle) +{ + float normalized = std::fmod(angle, 360.f); + if (normalized < 0.f) + normalized += 360.f; + return normalized; +} + +float texture_mapping_offset_fade_factor(int fade_mode, float progress01) +{ + const float p = clamp01f(progress01); + switch (fade_mode) { + case int(TextureMappingZone::OffsetFadeInUp): return p; + case int(TextureMappingZone::OffsetFadeOutUp): return 1.f - p; + case int(TextureMappingZone::OffsetFadeInOut): return 1.f - std::abs(2.f * p - 1.f); + case int(TextureMappingZone::OffsetFadeOutIn): return std::abs(2.f * p - 1.f); + case int(TextureMappingZone::OffsetFadeOutInReversed): return 2.f * p - 1.f; + default: return 1.f; + } +} + +float texture_mapping_offset_filament_strength_factor(const TextureMappingZone &zone, unsigned int physical_filament_id) +{ + if (physical_filament_id == 0) + return 1.f; + const size_t idx = size_t(physical_filament_id - 1); + if (idx >= zone.filament_strengths_pct.size()) + return 1.f; + const float strength_pct = zone.filament_strengths_pct[idx]; + if (!std::isfinite(strength_pct)) + return 1.f; + return std::clamp(strength_pct / 100.f, 0.f, 1.f); +} + +float texture_mapping_offset_filament_minimum_offset_factor(const TextureMappingZone &zone, unsigned int physical_filament_id) +{ + if (physical_filament_id == 0) + return 0.f; + const size_t idx = size_t(physical_filament_id - 1); + if (idx >= zone.filament_minimum_offsets_pct.size()) + return 0.f; + const float minimum_offset_pct = zone.filament_minimum_offsets_pct[idx]; + if (!std::isfinite(minimum_offset_pct)) + return 0.f; + return std::clamp(minimum_offset_pct / 100.f, 0.f, 1.f); +} + +std::optional build_texture_mapping_offset_context_for_layer( + const PrintObject &print_object, + const Layer &layer, + const TextureMappingZone &zone, + unsigned int texture_zone_id) +{ + const Print *print = print_object.print(); + if (print == nullptr) + return std::nullopt; + + const PrintConfig &print_config = print->config(); + const TextureMappingManager &texture_mgr = print->texture_mapping_manager(); + const size_t num_physical = print_config.filament_colour.values.size(); + if (num_physical == 0) + return std::nullopt; + + const bool vertex_color_match_mode = zone.is_image_texture(); + std::vector component_ids = decode_texture_mapping_offset_component_ids(zone, num_physical); + if (vertex_color_match_mode) { + const std::vector effective_component_ids = + TextureMappingManager::effective_texture_component_ids(zone, num_physical, print_config.filament_colour.values); + if (!effective_component_ids.empty()) + component_ids = effective_component_ids; + } + if (component_ids.empty()) + return std::nullopt; + + const int layer_index = int(layer.id()); + const int object_layer_count = int(print_object.layer_count()); + const float z_progress = object_layer_count > 1 ? + std::clamp(float(layer_index) / float(object_layer_count - 1), 0.f, 1.f) : + 0.f; + const unsigned int active_component_id = + texture_mgr.resolve_zone_component(texture_zone_id, num_physical, layer_index); + const auto active_component_it = std::find(component_ids.begin(), component_ids.end(), active_component_id); + if (active_component_it == component_ids.end()) + return std::nullopt; + const size_t active_component_idx = size_t(active_component_it - component_ids.begin()); + + const bool raw_texture_mapping_mode = + zone.texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues); + const int filament_color_mode = std::clamp(zone.filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorRGBKW)); + const int generic_solver_lookup_mode = std::clamp(zone.generic_solver_lookup_mode, + int(TextureMappingZone::GenericSolverClosestMix), + int(TextureMappingZone::GenericSolverBlendClosestTwo)); + const int generic_solver_mode = std::clamp(zone.generic_solver_mode, + int(TextureMappingZone::GenericSolverLegacy), + int(TextureMappingZone::GenericSolverV2)); + const int generic_solver_mix_model = std::clamp(zone.generic_solver_mix_model, + int(TextureMappingZone::GenericSolverPigmentPainter), + int(TextureMappingZone::GenericSolverPigmentPainter)); + const float texture_contrast_pct = std::clamp(zone.contrast_pct, 25.f, 300.f); + const float texture_tone_gamma = + (!std::isfinite(zone.tone_gamma) || zone.tone_gamma <= 0.f) ? + 1.f : + std::clamp(zone.tone_gamma, 0.5f, 3.f); + + std::vector> component_colors; + component_colors.reserve(component_ids.size()); + bool missing_component_color = false; + for (const unsigned int id : component_ids) { + if (id < 1 || id > print_config.filament_colour.values.size()) { + if (raw_texture_mapping_mode) + component_colors.push_back({ 0.f, 0.f, 0.f }); + else + missing_component_color = true; + continue; + } + ColorRGB decoded; + if (!decode_color(print_config.filament_colour.get_at(size_t(id - 1)), decoded)) { + if (raw_texture_mapping_mode) + component_colors.push_back({ 0.f, 0.f, 0.f }); + else + missing_component_color = true; + continue; + } + component_colors.push_back({ decoded.r(), decoded.g(), decoded.b() }); + } + if (vertex_color_match_mode && + (missing_component_color || component_colors.size() != component_ids.size() || component_colors.empty())) + return std::nullopt; + + std::vector reference_nozzles; + reference_nozzles.reserve(component_ids.size() + 2); + auto append_nozzle = [&reference_nozzles, &print_config](unsigned int component_id) { + if (component_id == 0) + return; + const size_t idx = size_t(component_id - 1); + if (idx < print_config.nozzle_diameter.values.size()) + reference_nozzles.emplace_back(float(print_config.nozzle_diameter.get_at(idx))); + }; + for (unsigned int id : component_ids) + append_nozzle(id); + append_nozzle(zone.component_a); + append_nozzle(zone.component_b); + + const float reference_nozzle = reference_nozzles.empty() ? + float(print_config.nozzle_diameter.values.empty() ? 0.4 : print_config.nozzle_diameter.values.front()) : + std::accumulate(reference_nozzles.begin(), reference_nozzles.end(), 0.f) / float(reference_nozzles.size()); + const float max_allowed_distance_mm = TextureMappingManager::max_component_surface_offset_mm(reference_nozzle); + if (max_allowed_distance_mm <= EPSILON) + return std::nullopt; + + std::vector distances_mm = TextureMappingManager::effective_offset_distances(zone, component_ids.size(), reference_nozzle); + std::vector angles_deg = TextureMappingManager::effective_offset_angles(zone, component_ids.size()); + if (distances_mm.size() != component_ids.size()) + distances_mm.assign(component_ids.size(), 0.f); + if (angles_deg.size() != component_ids.size()) + angles_deg = TextureMappingManager::default_offset_angles(component_ids.size()); + for (float &a : angles_deg) + a = normalize_texture_mapping_offset_angle_deg(a); + + bool has_nonzero_distance = false; + if (vertex_color_match_mode) { + distances_mm.assign(component_ids.size(), max_allowed_distance_mm); + has_nonzero_distance = max_allowed_distance_mm > EPSILON; + } else { + for (float &d : distances_mm) { + d = std::clamp(d, 0.f, max_allowed_distance_mm); + has_nonzero_distance = has_nonzero_distance || d > EPSILON; + } + } + if (!has_nonzero_distance) + return std::nullopt; + + const float global_strength_factor = + std::clamp(float(print_config.texture_mapping_outer_wall_gradient_global_strength.value) / 100.f, 0.f, 1.f); + if (global_strength_factor <= EPSILON) + return std::nullopt; + + const float base_outer_width_mm = + std::max(0.05f, float(print_config.texture_mapping_outer_wall_gradient_max_line_width.value)); + const float config_min_gradient_width_mm = std::clamp( + float(print_config.texture_mapping_outer_wall_gradient_min_line_width.value), + 0.05f, + base_outer_width_mm); + const float layer_height_mm = std::max(0.01f, float(layer.height)); + 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), + 0.05f, + base_outer_width_mm); + const float max_width_delta_mm = std::max(0.f, base_outer_width_mm - safe_min_gradient_width_mm); + const float effective_max_width_delta_mm = max_width_delta_mm * global_strength_factor; + const float max_width_delta_limit_mm = std::min(effective_max_width_delta_mm, 2.f * max_allowed_distance_mm); + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= EPSILON) + return std::nullopt; + + TextureMappingOffsetWeightField weight_field; + if (vertex_color_match_mode) { + const float layer_sample_falloff_mm = zone.high_resolution_sampling ? + std::max(0.03f, layer_height_mm * 0.5f) : + std::max(0.12f, layer_height_mm * 1.5f); + weight_field = build_texture_mapping_offset_weight_field(print_object, + component_colors, + raw_texture_mapping_mode, + filament_color_mode, + zone.force_sequential_filaments, + generic_solver_lookup_mode, + generic_solver_mode, + generic_solver_mix_model, + zone.use_legacy_fixed_color_mode, + texture_contrast_pct, + texture_tone_gamma, + float(layer.print_z), + layer_sample_falloff_mm, + zone.high_resolution_sampling); + if (weight_field.empty()) + return std::nullopt; + } + + const TransmissionDistanceCalibrationContext td_calibration_context = + transmission_distance_calibration_context(zone, component_ids, component_colors, filament_color_mode); + size_t previous_component_idx = size_t(-1); + if (layer_index > 0) { + const unsigned int previous_component_id = + texture_mgr.resolve_zone_component(texture_zone_id, num_physical, layer_index - 1); + const auto previous_component_it = std::find(component_ids.begin(), component_ids.end(), previous_component_id); + if (previous_component_it != component_ids.end()) + previous_component_idx = size_t(previous_component_it - component_ids.begin()); + } + + float rotation_deg = 0.f; + if (zone.offset_rotation_enabled) { + const float p = clamp01f(z_progress); + const float r = std::max(1.f, zone.offset_repeats); + float repeated_pos = p * r; + int segment_idx = int(std::floor(repeated_pos)); + float local = repeated_pos - float(segment_idx); + if (p >= 1.f - EPSILON) { + segment_idx = std::max(0, int(std::ceil(r)) - 1); + local = 1.f; + } + if (zone.offset_reverse_repeats && (segment_idx % 2 == 1)) + local = 1.f - local; + const float direction = zone.offset_clockwise ? -1.f : 1.f; + rotation_deg = direction * 360.f * zone.offset_rotations * clamp01f(local); + } + std::vector rotated_angles = angles_deg; + for (float &a : rotated_angles) + a = normalize_texture_mapping_offset_angle_deg(a + rotation_deg); + + const float fade_factor = std::abs(texture_mapping_offset_fade_factor(zone.offset_fade_mode, z_progress)); + if (fade_factor <= EPSILON) + return std::nullopt; + + TextureMappingOffsetContext context; + context.vertex_color_match_mode = vertex_color_match_mode; + context.object_center_mode = + !vertex_color_match_mode && + zone.offset_angle_mode != int(TextureMappingZone::OffsetAngleSurfaceNormal); + context.high_resolution_texture_sampling = zone.high_resolution_sampling; + context.compact_offset_mode = zone.compact_offset_mode; + context.nonlinear_offset_adjustment = zone.nonlinear_offset_adjustment; + context.object_center = print_object.bounding_box().center(); + context.active_component_id = active_component_id; + context.active_component_idx = active_component_idx; + context.component_ids = std::move(component_ids); + context.component_distances_mm = std::move(distances_mm); + context.rotated_angles = std::move(rotated_angles); + context.weight_field = std::move(weight_field); + context.inset_strength_reference_mm = max_allowed_distance_mm; + context.fade_factor = fade_factor; + context.max_width_delta_mm = max_width_delta_limit_mm; + context.active_component_strength_factor = texture_mapping_offset_filament_strength_factor(zone, active_component_id); + context.active_component_minimum_offset_factor = texture_mapping_offset_filament_minimum_offset_factor(zone, active_component_id); + context.active_component_td_width_factor = + transmission_distance_width_factor(td_calibration_context, active_component_idx, previous_component_idx); + context.base_outer_width_mm = base_outer_width_mm; + context.layer_height_mm = layer_height_mm; + context.layer = &layer; + return context; +} + +float texture_mapping_offset_surface_inset_mm(const TextureMappingOffsetContext &context, + const Point &point, + double inward_x, + double inward_y) +{ + if (!std::isfinite(inward_x) || !std::isfinite(inward_y)) + return 0.f; + + const double outward_x = -inward_x; + const double outward_y = -inward_y; + float inset_strength = 0.f; + if (context.vertex_color_match_mode) { + const float desired_strength = + sample_weight_field(context.weight_field, + unscale(point.x()), + unscale(point.y()), + context.active_component_idx, + context.high_resolution_texture_sampling, + context.compact_offset_mode); + inset_strength = std::clamp(1.f - desired_strength, 0.f, 1.f); + } else { + double theta_direction_x = outward_x; + double theta_direction_y = outward_y; + if (context.object_center_mode) { + const double radial_x = double(point.x()) - double(context.object_center.x()); + const double radial_y = double(point.y()) - double(context.object_center.y()); + const double radial_len = std::hypot(radial_x, radial_y); + if (radial_len > EPSILON) { + theta_direction_x = radial_x / radial_len; + theta_direction_y = radial_y / radial_len; + } + } + + const float theta_deg = + normalize_texture_mapping_offset_angle_deg(float(Geometry::rad2deg(std::atan2(theta_direction_y, theta_direction_x)))); + float raw_inset_mm = 0.f; + const size_t component_count = std::min(context.component_ids.size(), context.component_distances_mm.size()); + for (size_t i = 0; i < component_count; ++i) { + if (i == context.active_component_idx) + continue; + const float influence = + component_angular_influence(context.component_ids[i], + theta_deg, + context.component_ids, + context.rotated_angles); + raw_inset_mm += context.component_distances_mm[i] * influence; + } + inset_strength = std::clamp(raw_inset_mm / std::max(context.inset_strength_reference_mm, float(EPSILON)), 0.f, 1.f); + } + + inset_strength = std::clamp(inset_strength * context.fade_factor, 0.f, 1.f); + const float stair_step_mm = context.nonlinear_offset_adjustment ? + local_surface_stair_step_distance(context.layer, + point, + outward_x, + outward_y, + context.base_outer_width_mm, + context.inset_strength_reference_mm) : + std::numeric_limits::quiet_NaN(); + return variable_width_delta_for_visibility_range(inset_strength, + context.max_width_delta_mm, + context.active_component_minimum_offset_factor, + context.active_component_strength_factor, + context.active_component_td_width_factor, + context.nonlinear_offset_adjustment, + context.layer_height_mm, + stair_step_mm); +} + +} // namespace Slic3r diff --git a/src/libslic3r/TextureMappingOffset.hpp b/src/libslic3r/TextureMappingOffset.hpp new file mode 100644 index 00000000000..f422d6c8678 --- /dev/null +++ b/src/libslic3r/TextureMappingOffset.hpp @@ -0,0 +1,91 @@ +// original author: sentientstardust + +#ifndef slic3r_TextureMappingOffset_hpp_ +#define slic3r_TextureMappingOffset_hpp_ + +#include "libslic3r.h" +#include "Point.hpp" +#include "TextureMapping.hpp" + +#include +#include +#include +#include + +namespace Slic3r { + +class Layer; +class PrintObject; + +struct TextureMappingOffsetWeightField { + float min_x_mm { 0.f }; + float min_y_mm { 0.f }; + float bucket_width_mm { 1.f }; + float bucket_height_mm { 1.f }; + int bucket_width { 0 }; + int bucket_height { 0 }; + size_t component_count { 0 }; + std::vector sample_x_mm; + std::vector sample_y_mm; + std::vector sample_weight; + std::vector sample_component_weights; + std::vector> buckets; + std::vector fallback_weights; + bool raw_component_weights_from_texture { false }; + + bool empty() const + { + return bucket_width <= 0 || + bucket_height <= 0 || + component_count == 0 || + sample_x_mm.empty() || + sample_y_mm.size() != sample_x_mm.size() || + sample_weight.size() != sample_x_mm.size() || + sample_component_weights.size() != sample_x_mm.size() * component_count; + } +}; + +struct TextureMappingOffsetContext { + bool vertex_color_match_mode { false }; + bool object_center_mode { false }; + bool high_resolution_texture_sampling { false }; + bool compact_offset_mode { false }; + bool nonlinear_offset_adjustment { false }; + Point object_center; + unsigned int active_component_id { 0 }; + size_t active_component_idx { size_t(-1) }; + std::vector component_ids; + std::vector component_distances_mm; + std::vector rotated_angles; + TextureMappingOffsetWeightField weight_field; + float inset_strength_reference_mm { 0.f }; + float fade_factor { 1.f }; + float max_width_delta_mm { 0.f }; + float active_component_strength_factor { 1.f }; + float active_component_minimum_offset_factor { 0.f }; + float active_component_td_width_factor { 1.f }; + float base_outer_width_mm { 0.f }; + float layer_height_mm { 0.f }; + const Layer *layer { nullptr }; +}; + +std::vector decode_texture_mapping_offset_component_ids(const TextureMappingZone &zone, size_t num_physical); +float normalize_texture_mapping_offset_angle_deg(float angle); +float texture_mapping_offset_fade_factor(int fade_mode, float progress01); +float texture_mapping_offset_filament_strength_factor(const TextureMappingZone &zone, unsigned int physical_filament_id); +float texture_mapping_offset_filament_minimum_offset_factor(const TextureMappingZone &zone, unsigned int physical_filament_id); + +std::optional build_texture_mapping_offset_context_for_layer( + const PrintObject &print_object, + const Layer &layer, + const TextureMappingZone &zone, + unsigned int texture_zone_id); + +float texture_mapping_offset_surface_inset_mm(const TextureMappingOffsetContext &context, + const Point &point, + double inward_x, + double inward_y); + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ede93a4eda5..5a1fde472bc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1076,6 +1076,7 @@ public: bool reduce_outer_surface_texture, bool seam_hiding, bool nonlinear_offset_adjustment, + bool perimeter_path_modulation, bool compact_offset_mode, bool use_legacy_fixed_color_mode, bool high_speed_image_texture_sampling, @@ -1303,6 +1304,9 @@ public: m_nonlinear_offset_adjustment_checkbox->SetToolTip( _L("Adjusts line-width offsets using a surface-visibility model derived from Kuipers et al. 2018.")); experimental_box->Add(m_nonlinear_offset_adjustment_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + m_perimeter_path_modulation_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Use perimeter path modulation")); + m_perimeter_path_modulation_checkbox->SetValue(perimeter_path_modulation); + experimental_box->Add(m_perimeter_path_modulation_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); m_compact_offset_mode_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Compact Offset Mode")); m_compact_offset_mode_checkbox->SetValue(compact_offset_mode); m_compact_offset_mode_checkbox->SetToolTip( @@ -1551,6 +1555,7 @@ public: bool reduce_outer_surface_texture() const { return m_reduce_outer_surface_texture_checkbox && m_reduce_outer_surface_texture_checkbox->GetValue(); } bool seam_hiding() const { return m_seam_hiding_checkbox && m_seam_hiding_checkbox->GetValue(); } bool nonlinear_offset_adjustment() const { return m_nonlinear_offset_adjustment_checkbox && m_nonlinear_offset_adjustment_checkbox->GetValue(); } + bool perimeter_path_modulation() const { return m_perimeter_path_modulation_checkbox && m_perimeter_path_modulation_checkbox->GetValue(); } bool compact_offset_mode() const { return m_compact_offset_mode_checkbox && m_compact_offset_mode_checkbox->GetValue(); } bool use_legacy_fixed_color_mode() const { return m_use_legacy_fixed_color_mode_checkbox && m_use_legacy_fixed_color_mode_checkbox->GetValue(); } bool high_speed_image_texture_sampling() const { return m_high_speed_image_texture_sampling_checkbox == nullptr || m_high_speed_image_texture_sampling_checkbox->GetValue(); } @@ -1857,6 +1862,7 @@ private: wxCheckBox *m_reduce_outer_surface_texture_checkbox {nullptr}; wxCheckBox *m_seam_hiding_checkbox {nullptr}; wxCheckBox *m_nonlinear_offset_adjustment_checkbox {nullptr}; + wxCheckBox *m_perimeter_path_modulation_checkbox {nullptr}; wxCheckBox *m_compact_offset_mode_checkbox {nullptr}; wxCheckBox *m_use_legacy_fixed_color_mode_checkbox {nullptr}; wxCheckBox *m_high_speed_image_texture_sampling_checkbox {nullptr}; @@ -5964,6 +5970,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.reduce_outer_surface_texture, updated.seam_hiding, updated.nonlinear_offset_adjustment, + updated.perimeter_path_modulation, updated.compact_offset_mode, updated.use_legacy_fixed_color_mode, updated.high_speed_image_texture_sampling, @@ -5998,6 +6005,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.reduce_outer_surface_texture = dlg.reduce_outer_surface_texture(); updated.seam_hiding = dlg.seam_hiding(); updated.nonlinear_offset_adjustment = dlg.nonlinear_offset_adjustment(); + updated.perimeter_path_modulation = dlg.perimeter_path_modulation(); updated.compact_offset_mode = dlg.compact_offset_mode(); updated.use_legacy_fixed_color_mode = dlg.use_legacy_fixed_color_mode(); updated.high_speed_image_texture_sampling = dlg.high_speed_image_texture_sampling();