Add dithering option (experimental)

This commit is contained in:
sentientstardust
2026-05-18 04:11:51 +01:00
parent 6a3e29b21c
commit 1dd0532026
6 changed files with 1026 additions and 21 deletions

View File

@@ -6054,6 +6054,26 @@ static float overhang_filament_minimum_offset_factor_for_gcode(const TextureMapp
return std::clamp(minimum_offset_pct / 100.f, 0.f, 1.f);
}
static std::vector<float> overhang_component_strength_factors_for_gcode(const TextureMappingZone &zone,
const std::vector<unsigned int> &component_ids)
{
std::vector<float> factors;
factors.reserve(component_ids.size());
for (const unsigned int id : component_ids)
factors.emplace_back(overhang_filament_strength_factor_for_gcode(zone, id));
return factors;
}
static std::vector<float> overhang_component_minimum_offset_factors_for_gcode(const TextureMappingZone &zone,
const std::vector<unsigned int> &component_ids)
{
std::vector<float> factors;
factors.reserve(component_ids.size());
for (const unsigned int id : component_ids)
factors.emplace_back(overhang_filament_minimum_offset_factor_for_gcode(zone, id));
return factors;
}
struct TransmissionDistanceCalibrationContextForGCode {
bool enabled { false };
int mode { int(TextureMappingZone::TDCalibrationNone) };
@@ -6880,6 +6900,190 @@ static void apply_texture_contrast_to_mapped_components_for_gcode(std::vector<fl
}
}
static std::array<float, 3> apply_texture_contrast_to_rgb_for_gcode(const std::array<float, 3> &rgb, float contrast_factor)
{
const float clamped_contrast = std::clamp(contrast_factor, 0.25f, 3.f);
if (std::abs(clamped_contrast - 1.f) <= 1e-5f)
return {clamp01f_for_gcode(rgb[0]), clamp01f_for_gcode(rgb[1]), clamp01f_for_gcode(rgb[2])};
const float mean = (clamp01f_for_gcode(rgb[0]) + clamp01f_for_gcode(rgb[1]) + clamp01f_for_gcode(rgb[2])) / 3.f;
return {
clamp01f_for_gcode(mean + (clamp01f_for_gcode(rgb[0]) - mean) * clamped_contrast),
clamp01f_for_gcode(mean + (clamp01f_for_gcode(rgb[1]) - mean) * clamped_contrast),
clamp01f_for_gcode(mean + (clamp01f_for_gcode(rgb[2]) - mean) * clamped_contrast)
};
}
struct GCodeBinaryDitherCandidate {
uint32_t mask { 0 };
std::array<float, 3> rgb { { 1.f, 1.f, 1.f } };
std::array<float, 3> oklab { { 1.f, 0.f, 0.f } };
};
static std::vector<GCodeBinaryDitherCandidate> binary_dither_candidates_for_gcode(
const std::vector<std::array<float, 3>> &component_colors,
const std::vector<float> &component_strength_factors,
const std::vector<float> &component_minimum_offset_factors,
int generic_solver_mix_model)
{
std::vector<GCodeBinaryDitherCandidate> candidates;
const size_t component_count = component_colors.size();
if (component_count == 0 || component_count > 16)
return candidates;
const uint32_t mask_end = uint32_t(1) << component_count;
candidates.reserve(size_t(mask_end - 1));
for (uint32_t mask = 1; mask < mask_end; ++mask) {
std::vector<float> mix_weights(component_count, 0.f);
float total_weight = 0.f;
for (size_t component_idx = 0; component_idx < component_count; ++component_idx) {
const float strength = component_idx < component_strength_factors.size() ?
std::clamp(component_strength_factors[component_idx], 0.f, 1.f) :
1.f;
const float minimum = component_idx < component_minimum_offset_factors.size() ?
std::clamp(component_minimum_offset_factors[component_idx], 0.f, 1.f) :
0.f;
const bool active = (mask & (uint32_t(1) << component_idx)) != 0;
mix_weights[component_idx] = std::clamp(minimum + (active ? strength * (1.f - minimum) : 0.f), 0.f, 1.f);
total_weight += mix_weights[component_idx];
}
if (total_weight <= EPSILON) {
for (size_t component_idx = 0; component_idx < component_count; ++component_idx)
if ((mask & (uint32_t(1) << component_idx)) != 0)
mix_weights[component_idx] = 1.f;
}
GCodeBinaryDitherCandidate candidate;
candidate.mask = mask;
candidate.rgb = mix_color_solver_components(component_colors,
mix_weights,
color_solver_mix_model_from_index(generic_solver_mix_model));
candidate.oklab = oklab_from_srgb_for_gcode(candidate.rgb);
candidates.emplace_back(candidate);
}
return candidates;
}
struct GCodeBinaryDitherNearestResult {
size_t best_idx { size_t(-1) };
size_t second_idx { size_t(-1) };
float best_error { std::numeric_limits<float>::max() };
float second_error { std::numeric_limits<float>::max() };
};
static GCodeBinaryDitherNearestResult nearest_binary_dither_candidates_for_gcode(
const std::vector<GCodeBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab)
{
GCodeBinaryDitherNearestResult result;
const std::array<float, 3> axis_weights = generic_solver_v2_axis_weights_for_gcode(target_oklab);
for (size_t idx = 0; idx < candidates.size(); ++idx) {
const std::array<float, 3> &candidate = candidates[idx].oklab;
const float dl = candidate[0] - target_oklab[0];
const float da = candidate[1] - target_oklab[1];
const float db = candidate[2] - target_oklab[2];
const float error = axis_weights[0] * dl * dl + axis_weights[1] * da * da + axis_weights[2] * db * db;
if (error < result.best_error) {
result.second_error = result.best_error;
result.second_idx = result.best_idx;
result.best_error = error;
result.best_idx = idx;
} else if (error < result.second_error) {
result.second_error = error;
result.second_idx = idx;
}
}
return result;
}
static size_t nearest_binary_dither_candidate_for_gcode(const std::vector<GCodeBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab)
{
return nearest_binary_dither_candidates_for_gcode(candidates, target_oklab).best_idx;
}
static float binary_dither_alternate_fraction_for_gcode(const std::vector<GCodeBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab,
size_t base_idx,
size_t alternate_idx)
{
if (base_idx >= candidates.size() || alternate_idx >= candidates.size() || base_idx == alternate_idx)
return 0.f;
const std::array<float, 3> axis_weights = generic_solver_v2_axis_weights_for_gcode(target_oklab);
const std::array<float, 3> &base = candidates[base_idx].oklab;
const std::array<float, 3> &alternate = candidates[alternate_idx].oklab;
float numerator = 0.f;
float denominator = 0.f;
for (size_t axis = 0; axis < 3; ++axis) {
const float delta = alternate[axis] - base[axis];
numerator += axis_weights[axis] * (target_oklab[axis] - base[axis]) * delta;
denominator += axis_weights[axis] * delta * delta;
}
if (!std::isfinite(numerator) || !std::isfinite(denominator) || denominator <= 1e-12f)
return 0.f;
return std::clamp(numerator / denominator, 0.f, 1.f);
}
static size_t thresholded_binary_dither_candidate_for_gcode(const std::vector<GCodeBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab,
float threshold)
{
const GCodeBinaryDitherNearestResult nearest = nearest_binary_dither_candidates_for_gcode(candidates, target_oklab);
if (nearest.best_idx >= candidates.size())
return size_t(-1);
if (nearest.second_idx >= candidates.size())
return nearest.best_idx;
const float alternate_fraction =
binary_dither_alternate_fraction_for_gcode(candidates, target_oklab, nearest.best_idx, nearest.second_idx);
return std::clamp(threshold, 0.f, 1.f) < alternate_fraction ? nearest.second_idx : nearest.best_idx;
}
static float ordered_bayer_threshold_for_gcode(int x, int y)
{
static constexpr int matrix[8][8] = {
{ 0, 48, 12, 60, 3, 51, 15, 63 },
{ 32, 16, 44, 28, 35, 19, 47, 31 },
{ 8, 56, 4, 52, 11, 59, 7, 55 },
{ 40, 24, 36, 20, 43, 27, 39, 23 },
{ 2, 50, 14, 62, 1, 49, 13, 61 },
{ 34, 18, 46, 30, 33, 17, 45, 29 },
{ 10, 58, 6, 54, 9, 57, 5, 53 },
{ 42, 26, 38, 22, 41, 25, 37, 21 }
};
const int bx = ((x % 8) + 8) % 8;
const int by = ((y % 8) + 8) % 8;
return (float(matrix[by][bx]) + 0.5f) / 64.f - 0.5f;
}
static float halftone_threshold_for_gcode(float x_mm, float y_mm, float dot_size_mm)
{
const float period = std::clamp(dot_size_mm, 0.08f, 2.f);
const float u = x_mm / period - std::floor(x_mm / period);
const float v = y_mm / period - std::floor(y_mm / period);
const float dx = u - 0.5f;
const float dy = v - 0.5f;
const float radius_area = (dx * dx + dy * dy) / 0.5f;
return std::clamp(radius_area, 0.f, 1.f) - 0.5f;
}
static float dither_pitch_for_gcode(float base_outer_width_mm,
int dithering_method,
float dithering_resolution_mm,
float halftone_dot_size_mm)
{
const float high_res_step_mm = std::clamp(base_outer_width_mm * 0.20f, 0.04f, 0.12f);
const int clamped_method = std::clamp(dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone));
if (clamped_method == int(TextureMappingZone::DitheringHalftone)) {
const float dot_sample_step_mm = std::clamp(std::clamp(halftone_dot_size_mm, 0.08f, 2.f) * 0.25f, 0.04f, 0.08f);
return std::min(high_res_step_mm, dot_sample_step_mm);
}
return std::min(high_res_step_mm, std::clamp(dithering_resolution_mm, 0.04f, 0.25f));
}
static float wrap_repeat01_for_gcode(float uv)
{
if (!std::isfinite(uv))
@@ -8003,6 +8207,12 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
int generic_solver_mode,
int generic_solver_mix_model,
bool use_legacy_fixed_color_mode,
bool dithering_enabled,
int dithering_method,
float dither_pitch_mm,
float halftone_dot_size_mm,
const std::vector<float> &component_strength_factors,
const std::vector<float> &component_minimum_offset_factors,
std::map<std::string, GCodeGenericMixCandidateSet> *generic_mix_candidate_cache,
std::map<const PrintObject*, GCodeUVTextureTriangleCache> *uv_texture_triangle_cache,
float texture_contrast_pct,
@@ -8039,6 +8249,10 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
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);
const float physical_sample_pitch_mm =
dithering_enabled && !raw_values_mode && std::isfinite(dither_pitch_mm) && dither_pitch_mm > EPSILON ?
dither_pitch_mm :
(high_resolution_texture_sampling ? 0.08f : 0.16f);
struct TextureSampleData {
std::array<float, 4> rgba { { 0.f, 0.f, 0.f, 1.f } };
@@ -8170,7 +8384,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
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 float sample_pitch_mm = physical_sample_pitch_mm;
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) {
@@ -8216,8 +8430,9 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
if (!std::isfinite(tri_area_mm2))
return;
const float world_sample_pitch_mm = high_resolution_texture_sampling ? 0.08f : 0.16f;
const int max_bary_steps = high_resolution_texture_sampling ? 80 : 40;
const float world_sample_pitch_mm = physical_sample_pitch_mm;
const int max_bary_steps =
dithering_enabled && !raw_values_mode ? 160 : (high_resolution_texture_sampling ? 80 : 40);
const int bary_steps = std::clamp(int(std::ceil(max_world_edge_mm / world_sample_pitch_mm)), 1, max_bary_steps);
const int sample_count = bary_steps * (bary_steps + 1) / 2;
if (sample_count <= 0)
@@ -8376,8 +8591,9 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
return false;
const float uv_texels_per_step = high_resolution_texture_sampling ? 8.f : 18.f;
const float world_sample_pitch_mm = high_resolution_texture_sampling ? 0.08f : 0.16f;
const int max_bary_steps = high_resolution_texture_sampling ? 80 : 40;
const float world_sample_pitch_mm = physical_sample_pitch_mm;
const int max_bary_steps =
dithering_enabled && !raw_values_mode ? 160 : (high_resolution_texture_sampling ? 80 : 40);
const int uv_steps = std::clamp(int(std::ceil(tri.max_uv_edge_texel / uv_texels_per_step)), 1, max_bary_steps);
const int world_steps = std::clamp(int(std::ceil(tri.max_world_edge_mm / world_sample_pitch_mm)), 1, max_bary_steps);
const int bary_steps = std::max(uv_steps, world_steps);
@@ -8518,6 +8734,158 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
if (samples.empty())
return VertexColorOverhangWeightField{};
std::vector<uint32_t> binary_dither_masks;
const bool can_binary_dither =
dithering_enabled &&
!raw_values_mode &&
component_count > 0 &&
std::isfinite(dither_pitch_mm) &&
dither_pitch_mm > EPSILON;
if (can_binary_dither) {
bool has_raw_samples = false;
for (const WeightedTextureSample &sample : samples) {
if (sample.raw_component_weights_from_texture || sample.raw_component_weights.size() == component_count) {
has_raw_samples = true;
break;
}
}
const std::vector<GCodeBinaryDitherCandidate> binary_candidates =
has_raw_samples ?
std::vector<GCodeBinaryDitherCandidate>{} :
binary_dither_candidates_for_gcode(component_colors,
component_strength_factors,
component_minimum_offset_factors,
generic_solver_mix_model);
if (!binary_candidates.empty()) {
struct BinaryDitherCell {
int x { 0 };
int y { 0 };
float weight { 0.f };
std::array<float, 3> target_oklab { { 0.f, 0.f, 0.f } };
std::vector<size_t> sample_indices;
};
auto sample_target_oklab = [tone_gamma, contrast_factor](const WeightedTextureSample &sample) {
std::array<float, 3> target = {
clamp01f_for_gcode(sample.rgba[0]),
clamp01f_for_gcode(sample.rgba[1]),
clamp01f_for_gcode(sample.rgba[2])
};
if (std::abs(tone_gamma - 1.f) > 1e-5f) {
target[0] = apply_texture_tone_gamma_for_gcode(target[0], tone_gamma);
target[1] = apply_texture_tone_gamma_for_gcode(target[1], tone_gamma);
target[2] = apply_texture_tone_gamma_for_gcode(target[2], tone_gamma);
}
target = apply_texture_contrast_to_rgb_for_gcode(target, contrast_factor);
return oklab_from_srgb_for_gcode(target);
};
std::vector<BinaryDitherCell> cells;
std::map<std::pair<int, int>, size_t> cell_index_by_coord;
for (size_t sample_idx = 0; sample_idx < samples.size(); ++sample_idx) {
const WeightedTextureSample &sample = samples[sample_idx];
if (sample.weight <= EPSILON)
continue;
const int cell_x = int(std::floor((sample.x_mm - min_x_mm) / dither_pitch_mm));
const int cell_y = int(std::floor((sample.y_mm - min_y_mm) / dither_pitch_mm));
const std::pair<int, int> key(cell_x, cell_y);
auto cell_it = cell_index_by_coord.find(key);
if (cell_it == cell_index_by_coord.end()) {
cell_it = cell_index_by_coord.emplace(key, cells.size()).first;
BinaryDitherCell cell;
cell.x = cell_x;
cell.y = cell_y;
cells.emplace_back(std::move(cell));
}
BinaryDitherCell &cell = cells[cell_it->second];
const std::array<float, 3> target_oklab = sample_target_oklab(sample);
const float sample_weight = std::max(sample.weight, 0.f);
for (size_t axis = 0; axis < 3; ++axis)
cell.target_oklab[axis] += target_oklab[axis] * sample_weight;
cell.weight += sample_weight;
cell.sample_indices.emplace_back(sample_idx);
}
if (!cells.empty()) {
for (BinaryDitherCell &cell : cells)
if (cell.weight > EPSILON)
for (float &value : cell.target_oklab)
value /= cell.weight;
std::vector<size_t> order(cells.size(), 0);
std::iota(order.begin(), order.end(), size_t(0));
std::sort(order.begin(), order.end(), [&cells](size_t lhs, size_t rhs) {
if (cells[lhs].y != cells[rhs].y)
return cells[lhs].y < cells[rhs].y;
return cells[lhs].x < cells[rhs].x;
});
binary_dither_masks.assign(samples.size(), 0);
const int clamped_dither_method =
std::clamp(dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone));
std::map<std::pair<int, int>, std::array<float, 3>> floyd_error;
for (const size_t cell_idx : order) {
BinaryDitherCell &cell = cells[cell_idx];
std::array<float, 3> target_oklab = cell.target_oklab;
if (clamped_dither_method == int(TextureMappingZone::DitheringFloydSteinberg)) {
const auto error_it = floyd_error.find({cell.x, cell.y});
if (error_it != floyd_error.end())
for (size_t axis = 0; axis < 3; ++axis)
target_oklab[axis] += error_it->second[axis];
}
bool thresholded_dither = false;
float threshold = 0.f;
if (clamped_dither_method == int(TextureMappingZone::DitheringOrderedBayer)) {
thresholded_dither = true;
threshold = ordered_bayer_threshold_for_gcode(cell.x, cell.y) + 0.5f;
} else if (clamped_dither_method == int(TextureMappingZone::DitheringHalftone)) {
const float x_mm = min_x_mm + (float(cell.x) + 0.5f) * dither_pitch_mm;
const float y_mm = min_y_mm + (float(cell.y) + 0.5f) * dither_pitch_mm;
thresholded_dither = true;
threshold = halftone_threshold_for_gcode(x_mm, y_mm, halftone_dot_size_mm) + 0.5f;
}
const size_t candidate_idx =
thresholded_dither ?
thresholded_binary_dither_candidate_for_gcode(binary_candidates, target_oklab, threshold) :
nearest_binary_dither_candidate_for_gcode(binary_candidates, target_oklab);
if (candidate_idx >= binary_candidates.size())
continue;
const GCodeBinaryDitherCandidate &candidate = binary_candidates[candidate_idx];
for (const size_t sample_idx : cell.sample_indices)
if (sample_idx < binary_dither_masks.size())
binary_dither_masks[sample_idx] = candidate.mask;
if (clamped_dither_method == int(TextureMappingZone::DitheringFloydSteinberg)) {
std::array<float, 3> error = {
target_oklab[0] - candidate.oklab[0],
target_oklab[1] - candidate.oklab[1],
target_oklab[2] - candidate.oklab[2]
};
auto add_error = [&floyd_error, &cell_index_by_coord, &error](int x, int y, float factor) {
if (cell_index_by_coord.find({x, y}) == cell_index_by_coord.end())
return;
std::array<float, 3> &dst = floyd_error[{x, y}];
for (size_t axis = 0; axis < 3; ++axis)
dst[axis] += error[axis] * factor;
};
add_error(cell.x + 1, cell.y, 7.f / 16.f);
add_error(cell.x - 1, cell.y + 1, 3.f / 16.f);
add_error(cell.x, cell.y + 1, 5.f / 16.f);
add_error(cell.x + 1, cell.y + 1, 1.f / 16.f);
}
}
}
}
}
const std::vector<std::array<float, 3>> fixed_color_solver_component_colors =
fixed_color_generic_solver_component_colors_for_gcode(filament_color_mode);
const bool use_fixed_color_generic_solver =
@@ -8560,6 +8928,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
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;
weight_field.binary_dithered = !binary_dither_masks.empty();
std::vector<float> fallback_acc(component_count, 0.f);
float fallback_weight = 0.f;
@@ -8577,7 +8946,12 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
std::vector<float> 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) {
const bool has_binary_dither = sample_idx < binary_dither_masks.size() && binary_dither_masks[sample_idx] != 0;
if (has_binary_dither) {
const uint32_t mask = binary_dither_masks[sample_idx];
for (size_t component_idx = 0; component_idx < component_count; ++component_idx)
desired[component_idx] = (mask & (uint32_t(1) << component_idx)) != 0 ? 1.f : 0.f;
} else 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_for_gcode(sample.raw_component_weights[component_idx]);
@@ -8626,7 +9000,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(
}
}
if (!has_raw_component_weights && std::abs(contrast_factor - 1.f) > 1e-5f)
if (!has_binary_dither && !has_raw_component_weights && std::abs(contrast_factor - 1.f) > 1e-5f)
apply_texture_contrast_to_mapped_components_for_gcode(desired, contrast_factor, mapped_component_count);
for (size_t component_idx = 0; component_idx < component_count; ++component_idx) {
@@ -8695,6 +9069,68 @@ static std::vector<float> sample_vertex_color_weight_field_components_for_gcode(
const int cx = std::clamp(int(std::floor(gx_unclamped)), 0, weight_field.bucket_width - 1);
const int cy = std::clamp(int(std::floor(gy_unclamped)), 0, weight_field.bucket_height - 1);
if (weight_field.binary_dithered) {
float nearest_d2 = std::numeric_limits<float>::max();
size_t nearest_sample_idx = size_t(-1);
const int nearest_ring_limit = std::min(16, std::max(weight_field.bucket_width, weight_field.bucket_height));
for (int ring = 0; ring <= nearest_ring_limit; ++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);
auto visit_bucket = [&weight_field, x_mm, y_mm, &nearest_d2, &nearest_sample_idx](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())
continue;
const float dx = x_mm - weight_field.sample_x_mm[sample_idx];
const float dy = y_mm - weight_field.sample_y_mm[sample_idx];
const float d2 = dx * dx + dy * dy;
if (d2 >= nearest_d2)
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;
nearest_d2 = d2;
nearest_sample_idx = sample_idx;
}
};
if (ring == 0) {
visit_bucket(cx, cy);
} else {
for (int x = min_x; x <= max_x; ++x) {
visit_bucket(x, min_y);
if (max_y != min_y)
visit_bucket(x, max_y);
}
for (int y = min_y + 1; y <= max_y - 1; ++y) {
visit_bucket(min_x, y);
if (max_x != min_x)
visit_bucket(max_x, y);
}
}
if (nearest_sample_idx != size_t(-1))
break;
}
if (nearest_sample_idx != size_t(-1)) {
std::vector<float> values(weight_field.component_count, 0.f);
const size_t value_idx = nearest_sample_idx * weight_field.component_count;
for (size_t component_idx = 0; component_idx < weight_field.component_count; ++component_idx)
values[component_idx] = clamp01f_for_gcode(weight_field.sample_component_weights[value_idx + component_idx]);
return values;
}
return fallback;
}
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);
@@ -9171,7 +9607,21 @@ std::optional<PreferredSeamPoint> GCode::texture_mapping_seam_hiding_hint(const
int(TextureMappingZone::GenericSolverLegacy),
int(TextureMappingZone::GenericSolverV2));
const int generic_solver_mix_model = TextureMappingZone::DefaultGenericSolverMixModel;
const bool compact_offset_mode = zone->compact_offset_mode;
const bool dithering_enabled =
zone->dithering_enabled &&
zone->texture_mapping_mode != int(TextureMappingZone::TextureMappingRawValues);
const int dithering_method = std::clamp(zone->dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone));
const float seam_texture_base_width_mm =
std::max(0.05f, float(m_config.texture_mapping_outer_wall_gradient_max_line_width.value));
const float dither_pitch_mm =
dither_pitch_for_gcode(seam_texture_base_width_mm,
dithering_method,
zone->dithering_resolution_mm,
zone->halftone_dot_size_mm);
const float halftone_dot_size_mm = std::clamp(zone->halftone_dot_size_mm, 0.08f, 2.f);
const bool compact_offset_mode = zone->compact_offset_mode || dithering_enabled;
const bool nonlinear_offset_adjustment = zone->nonlinear_offset_adjustment;
const bool use_legacy_fixed_color_mode = zone->use_legacy_fixed_color_mode;
const float texture_contrast_pct = std::clamp(zone->contrast_pct, 25.f, 300.f);
@@ -9179,8 +9629,11 @@ std::optional<PreferredSeamPoint> GCode::texture_mapping_seam_hiding_hint(const
(!std::isfinite(zone->tone_gamma) || zone->tone_gamma <= 0.f) ?
1.f :
std::clamp(zone->tone_gamma, 0.5f, 3.f);
const bool high_resolution_texture_sampling = zone->high_resolution_sampling;
const bool high_resolution_texture_sampling = zone->high_resolution_sampling || dithering_enabled;
const bool high_speed_image_texture_sampling = zone->high_speed_image_texture_sampling;
const std::vector<float> component_strength_factors = overhang_component_strength_factors_for_gcode(*zone, component_ids);
const std::vector<float> component_minimum_offset_factors =
overhang_component_minimum_offset_factors_for_gcode(*zone, component_ids);
std::vector<std::array<float, 3>> component_colors;
component_colors.reserve(component_ids.size());
@@ -9225,6 +9678,17 @@ std::optional<PreferredSeamPoint> GCode::texture_mapping_seam_hiding_hint(const
component_key_stream << "|gl" << generic_solver_lookup_mode;
component_key_stream << "|gm" << generic_solver_mode;
component_key_stream << "|gx" << generic_solver_mix_model;
component_key_stream << "|de" << (dithering_enabled ? 1 : 0);
component_key_stream << "|dm" << dithering_method;
component_key_stream << "|dp" << int(std::lround(dither_pitch_mm * 1000.f));
if (dithering_method == int(TextureMappingZone::DitheringHalftone))
component_key_stream << "|hdt" << int(std::lround(halftone_dot_size_mm * 1000.f));
else
component_key_stream << "|hrz" << int(std::lround(std::clamp(zone->dithering_resolution_mm, 0.04f, 0.25f) * 1000.f));
for (const float strength_factor : component_strength_factors)
component_key_stream << "|st" << int(std::lround(std::clamp(strength_factor, 0.f, 1.f) * 1000.f));
for (const float minimum_offset_factor : component_minimum_offset_factors)
component_key_stream << "|mo" << int(std::lround(std::clamp(minimum_offset_factor, 0.f, 1.f) * 1000.f));
component_key_stream << "|lf" << (use_legacy_fixed_color_mode ? 1 : 0);
component_key_stream << "|ct" << int(std::lround(texture_contrast_pct));
component_key_stream << "|tg" << int(std::lround(texture_tone_gamma * 100.f));
@@ -9293,6 +9757,12 @@ std::optional<PreferredSeamPoint> GCode::texture_mapping_seam_hiding_hint(const
generic_solver_mode,
generic_solver_mix_model,
use_legacy_fixed_color_mode,
dithering_enabled,
dithering_method,
dither_pitch_mm,
halftone_dot_size_mm,
component_strength_factors,
component_minimum_offset_factors,
&m_generic_solver_mix_candidate_cache,
&m_uv_texture_triangle_cache,
texture_contrast_pct,
@@ -10450,6 +10920,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
bool enabled { false };
bool vertex_color_match_mode { false };
bool high_resolution_texture_sampling { false };
bool dithering_enabled { false };
bool nonlinear_offset_adjustment { false };
bool compact_offset_mode { false };
bool object_center_mode { false };
@@ -10468,6 +10939,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
float active_component_minimum_offset_factor { 0.f };
float active_component_td_width_factor { 1.f };
float base_outer_width_mm { 0.4f };
float dither_pitch_mm { 0.08f };
float flow_reference_width_mm { 0.4f };
float base_centerline_shift_mm { 0.f };
float centerline_shift_balance_mm { 0.f };
@@ -10653,10 +11125,22 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
!vertex_color_match_mode &&
zone->offset_angle_mode != int(TextureMappingZone::OffsetAngleSurfaceNormal);
const bool use_layer_aware_weighting = m_layer != nullptr;
const bool high_resolution_texture_sampling = zone->high_resolution_sampling;
const bool raw_texture_mapping_mode =
zone->texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues);
const bool dithering_enabled = zone->dithering_enabled && !raw_texture_mapping_mode;
const int dithering_method = std::clamp(zone->dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone));
const float dither_pitch_mm =
dither_pitch_for_gcode(base_outer_width_mm,
dithering_method,
zone->dithering_resolution_mm,
zone->halftone_dot_size_mm);
const float halftone_dot_size_mm = std::clamp(zone->halftone_dot_size_mm, 0.08f, 2.f);
const bool high_resolution_texture_sampling = zone->high_resolution_sampling || dithering_enabled;
const bool high_speed_image_texture_sampling = zone->high_speed_image_texture_sampling;
const bool nonlinear_offset_adjustment = zone->nonlinear_offset_adjustment;
const bool compact_offset_mode = zone->compact_offset_mode;
const bool compact_offset_mode = zone->compact_offset_mode || dithering_enabled;
const float layer_sample_z_mm = use_layer_aware_weighting ? float(m_layer->print_z) : 0.f;
const float layer_sample_falloff_mm = high_resolution_texture_sampling ?
std::max(0.03f, layer_height_mm * 0.5f) :
@@ -10682,8 +11166,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
const bool reduce_outer_surface_texture =
vertex_color_match_mode && zone->reduce_outer_surface_texture && !compact_offset_mode;
const bool raw_texture_mapping_mode =
zone->texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues);
std::vector<std::array<float, 3>> component_colors;
component_colors.reserve(component_ids.size());
bool missing_component_color = false;
@@ -10710,6 +11192,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
component_ids,
component_colors,
texture_filament_color_mode);
const std::vector<float> component_strength_factors =
overhang_component_strength_factors_for_gcode(*zone, component_ids);
const std::vector<float> component_minimum_offset_factors =
overhang_component_minimum_offset_factors_for_gcode(*zone, component_ids);
size_t previous_component_idx = size_t(-1);
if (current_layer_index > 0) {
const unsigned int previous_component_id =
@@ -10740,6 +11226,17 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
component_key_stream << "|gl" << generic_solver_lookup_mode;
component_key_stream << "|gm" << generic_solver_mode;
component_key_stream << "|gx" << generic_solver_mix_model;
component_key_stream << "|de" << (dithering_enabled ? 1 : 0);
component_key_stream << "|dm" << dithering_method;
component_key_stream << "|dp" << int(std::lround(dither_pitch_mm * 1000.f));
if (dithering_method == int(TextureMappingZone::DitheringHalftone))
component_key_stream << "|hdt" << int(std::lround(halftone_dot_size_mm * 1000.f));
else
component_key_stream << "|hrz" << int(std::lround(std::clamp(zone->dithering_resolution_mm, 0.04f, 0.25f) * 1000.f));
for (const float strength_factor : component_strength_factors)
component_key_stream << "|st" << int(std::lround(std::clamp(strength_factor, 0.f, 1.f) * 1000.f));
for (const float minimum_offset_factor : component_minimum_offset_factors)
component_key_stream << "|mo" << int(std::lround(std::clamp(minimum_offset_factor, 0.f, 1.f) * 1000.f));
component_key_stream << "|lf" << (use_legacy_fixed_color_mode ? 1 : 0);
component_key_stream << "|ct" << int(std::lround(texture_contrast_pct));
component_key_stream << "|tg" << int(std::lround(texture_tone_gamma * 100.f));
@@ -10761,6 +11258,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
generic_solver_mode,
generic_solver_mix_model,
use_legacy_fixed_color_mode,
dithering_enabled,
dithering_method,
dither_pitch_mm,
halftone_dot_size_mm,
component_strength_factors,
component_minimum_offset_factors,
generic_mix_candidate_cache,
&m_uv_texture_triangle_cache,
texture_contrast_pct,
@@ -10789,6 +11292,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
outer_wall_gradient_dynamic_ctx.enabled = true;
outer_wall_gradient_dynamic_ctx.vertex_color_match_mode = vertex_color_match_mode;
outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling = high_resolution_texture_sampling;
outer_wall_gradient_dynamic_ctx.dithering_enabled = dithering_enabled;
outer_wall_gradient_dynamic_ctx.nonlinear_offset_adjustment = nonlinear_offset_adjustment;
outer_wall_gradient_dynamic_ctx.compact_offset_mode = compact_offset_mode;
outer_wall_gradient_dynamic_ctx.object_center_mode = object_center_mode;
@@ -10807,6 +11311,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
outer_wall_gradient_dynamic_ctx.active_component_minimum_offset_factor = active_component_minimum_offset_factor;
outer_wall_gradient_dynamic_ctx.active_component_td_width_factor = active_component_td_width_factor;
outer_wall_gradient_dynamic_ctx.base_outer_width_mm = base_outer_width_mm;
outer_wall_gradient_dynamic_ctx.dither_pitch_mm = dither_pitch_mm;
outer_wall_gradient_dynamic_ctx.flow_reference_width_mm = flow_reference_width_mm;
outer_wall_gradient_dynamic_ctx.base_centerline_shift_mm = base_centerline_shift_mm;
outer_wall_gradient_dynamic_ctx.layer_height_mm = layer_height_mm;
@@ -11827,9 +12332,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
(outer_wall_gradient_dynamic_ctx.object_center_mode ||
outer_wall_gradient_dynamic_ctx.vertex_color_match_mode);
if (dynamic_line_modulation) {
const double high_res_modulation_step_mm =
outer_wall_gradient_dynamic_ctx.dithering_enabled ?
std::clamp(double(outer_wall_gradient_dynamic_ctx.dither_pitch_mm), 0.04, 0.12) :
std::clamp(double(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) * 0.20, 0.04, 0.12);
const double modulation_step_mm = outer_wall_gradient_dynamic_ctx.vertex_color_match_mode ?
(outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling ?
std::clamp(double(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) * 0.20, 0.04, 0.12) :
high_res_modulation_step_mm :
std::clamp(double(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) * 0.35, 0.05, 0.25)) :
std::clamp(double(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) * 2.0, 0.40, 1.20);
const double modulation_step_scaled = scale_(modulation_step_mm);
@@ -12162,7 +12671,9 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
if (high_res_dynamic_subsegments) {
const Line parent_line(pre_processed_point.p, processed_point.p);
const double modulation_step_mm =
std::clamp(double(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) * 0.20, 0.04, 0.12);
outer_wall_gradient_dynamic_ctx.dithering_enabled ?
std::clamp(double(outer_wall_gradient_dynamic_ctx.dither_pitch_mm), 0.04, 0.12) :
std::clamp(double(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) * 0.20, 0.04, 0.12);
const double modulation_step_scaled = scale_(modulation_step_mm);
const int subsegment_count =
std::clamp(int(std::ceil(parent_line.length() / std::max(modulation_step_scaled, EPSILON))), 1, 10000);

View File

@@ -68,6 +68,7 @@ struct VertexColorOverhangWeightField {
std::vector<std::vector<uint32_t>> buckets;
std::vector<float> fallback_weights;
bool raw_component_weights_from_texture { false };
bool binary_dithered { false };
bool empty() const
{

View File

@@ -599,6 +599,30 @@ static int generic_solver_mix_model_from_name(std::string)
return TextureMappingZone::DefaultGenericSolverMixModel;
}
static std::string dithering_method_name(int mode)
{
switch (clamp_int(mode,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone))) {
case int(TextureMappingZone::DitheringClosest): return "closest";
case int(TextureMappingZone::DitheringOrderedBayer): return "ordered_bayer";
case int(TextureMappingZone::DitheringHalftone): return "halftone";
default: return "floyd_steinberg";
}
}
static int dithering_method_from_name(std::string name)
{
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return char(std::tolower(c)); });
if (name == "closest" || name == "closest_combination")
return int(TextureMappingZone::DitheringClosest);
if (name == "ordered_bayer" || name == "bayer")
return int(TextureMappingZone::DitheringOrderedBayer);
if (name == "halftone")
return int(TextureMappingZone::DitheringHalftone);
return int(TextureMappingZone::DitheringFloydSteinberg);
}
static std::string transmission_distance_calibration_mode_name(int mode)
{
switch (clamp_int(mode,
@@ -802,6 +826,10 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const
generic_solver_lookup_mode == rhs.generic_solver_lookup_mode &&
generic_solver_mode == rhs.generic_solver_mode &&
generic_solver_mix_model == rhs.generic_solver_mix_model &&
dithering_enabled == rhs.dithering_enabled &&
dithering_method == rhs.dithering_method &&
std::abs(dithering_resolution_mm - rhs.dithering_resolution_mm) <= eps &&
std::abs(halftone_dot_size_mm - rhs.halftone_dot_size_mm) <= eps &&
std::abs(contrast_pct - rhs.contrast_pct) <= eps &&
high_resolution_sampling == rhs.high_resolution_sampling &&
std::abs(tone_gamma - rhs.tone_gamma) <= eps &&
@@ -1052,6 +1080,12 @@ std::string TextureMappingManager::serialize_entries()
texture["generic_solver_lookup"] = generic_solver_lookup_mode_name(zone.generic_solver_lookup_mode);
texture["generic_solver_mode"] = generic_solver_mode_name(zone.generic_solver_mode);
texture["generic_solver_mix_model"] = generic_solver_mix_model_name(zone.generic_solver_mix_model);
texture["dithering_enabled"] = zone.dithering_enabled;
texture["dithering_method"] = dithering_method_name(zone.dithering_method);
texture["dithering_resolution_mm"] =
std::clamp(finite_or(zone.dithering_resolution_mm, TextureMappingZone::DefaultDitheringResolutionMm), 0.04f, 0.25f);
texture["halftone_dot_size_mm"] =
std::clamp(finite_or(zone.halftone_dot_size_mm, TextureMappingZone::DefaultHalftoneDotSizeMm), 0.08f, 2.f);
texture["contrast_pct"] = std::clamp(finite_or(zone.contrast_pct, 100.f), 25.f, 300.f);
texture["high_resolution_sampling"] = zone.high_resolution_sampling;
texture["tone_gamma"] = normalize_tone_gamma(zone.tone_gamma);
@@ -1197,6 +1231,15 @@ void TextureMappingManager::load_entries(const std::string &serialized,
int(TextureMappingZone::GenericSolverV2));
zone.generic_solver_mix_model =
generic_solver_mix_model_from_name(texture.value("generic_solver_mix_model", std::string("pigment_painter")));
zone.dithering_enabled = texture.value("dithering_enabled", TextureMappingZone::DefaultDitheringEnabled);
zone.dithering_method = dithering_method_from_name(
texture.value("dithering_method", dithering_method_name(TextureMappingZone::DefaultDitheringMethod)));
zone.dithering_resolution_mm =
std::clamp(texture.value("dithering_resolution_mm", TextureMappingZone::DefaultDitheringResolutionMm), 0.04f, 0.25f);
zone.halftone_dot_size_mm =
std::clamp(texture.value("halftone_dot_size_mm", TextureMappingZone::DefaultHalftoneDotSizeMm), 0.08f, 2.f);
if (zone.dithering_enabled)
zone.compact_offset_mode = true;
zone.contrast_pct = std::clamp(texture.value("contrast_pct", 100.f), 25.f, 300.f);
zone.high_resolution_sampling = texture.value("high_resolution_sampling", true);
zone.tone_gamma = normalize_tone_gamma(texture.value("tone_gamma", 1.f));

View File

@@ -73,6 +73,13 @@ struct TextureMappingZone
GenericSolverPigmentPainter = 0
};
enum DitheringMethod : uint8_t {
DitheringClosest = 0,
DitheringFloydSteinberg = 1,
DitheringOrderedBayer = 2,
DitheringHalftone = 3
};
enum TransmissionDistanceCalibrationMode : uint8_t {
TDCalibrationNone = 0,
TDCalibrationAbsolute = 1,
@@ -102,6 +109,10 @@ struct TextureMappingZone
static constexpr int DefaultGenericSolverLookupMode = int(GenericSolverClosestMix);
static constexpr int DefaultGenericSolverMode = int(GenericSolverV2);
static constexpr int DefaultGenericSolverMixModel = int(GenericSolverPigmentPainter);
static constexpr bool DefaultDitheringEnabled = false;
static constexpr int DefaultDitheringMethod = int(DitheringFloydSteinberg);
static constexpr float DefaultDitheringResolutionMm = 0.08f;
static constexpr float DefaultHalftoneDotSizeMm = 0.32f;
static constexpr float DefaultContrastPct = 100.f;
static constexpr bool DefaultHighResolutionSampling = true;
static constexpr float DefaultToneGamma = 1.f;
@@ -146,6 +157,10 @@ struct TextureMappingZone
int generic_solver_lookup_mode = DefaultGenericSolverLookupMode;
int generic_solver_mode = DefaultGenericSolverMode;
int generic_solver_mix_model = DefaultGenericSolverMixModel;
bool dithering_enabled = DefaultDitheringEnabled;
int dithering_method = DefaultDitheringMethod;
float dithering_resolution_mm = DefaultDitheringResolutionMm;
float halftone_dot_size_mm = DefaultHalftoneDotSizeMm;
float contrast_pct = DefaultContrastPct;
bool high_resolution_sampling = DefaultHighResolutionSampling;
float tone_gamma = DefaultToneGamma;
@@ -191,6 +206,10 @@ struct TextureMappingZone
generic_solver_lookup_mode = DefaultGenericSolverLookupMode;
generic_solver_mode = DefaultGenericSolverMode;
generic_solver_mix_model = DefaultGenericSolverMixModel;
dithering_enabled = DefaultDitheringEnabled;
dithering_method = DefaultDitheringMethod;
dithering_resolution_mm = DefaultDitheringResolutionMm;
halftone_dot_size_mm = DefaultHalftoneDotSizeMm;
contrast_pct = DefaultContrastPct;
high_resolution_sampling = DefaultHighResolutionSampling;
tone_gamma = DefaultToneGamma;

View File

@@ -73,6 +73,10 @@ struct TexturePreviewSimulationSettings
bool compact_offset_mode = false;
bool use_legacy_fixed_color_mode = false;
bool use_fixed_color_generic_solver = false;
bool dithering_enabled = false;
int dithering_method = TextureMappingZone::DefaultDitheringMethod;
float dithering_resolution_mm = TextureMappingZone::DefaultDitheringResolutionMm;
float halftone_dot_size_mm = TextureMappingZone::DefaultHalftoneDotSizeMm;
float contrast_pct = 100.f;
float tone_gamma = 1.f;
int generic_solver_lookup_mode = int(TextureMappingZone::GenericSolverClosestMix);
@@ -1435,6 +1439,199 @@ void apply_texture_contrast_to_mapped_components(std::vector<float> &component_w
}
}
std::array<float, 3> apply_texture_contrast_to_rgb(const std::array<float, 3> &rgb, float contrast_factor)
{
const float clamped_contrast = std::clamp(contrast_factor, 0.25f, 3.f);
if (std::abs(clamped_contrast - 1.f) <= 1e-5f)
return { clamp01(rgb[0]), clamp01(rgb[1]), clamp01(rgb[2]) };
const float mean = (clamp01(rgb[0]) + clamp01(rgb[1]) + clamp01(rgb[2])) / 3.f;
return {
clamp01(mean + (clamp01(rgb[0]) - mean) * clamped_contrast),
clamp01(mean + (clamp01(rgb[1]) - mean) * clamped_contrast),
clamp01(mean + (clamp01(rgb[2]) - mean) * clamped_contrast)
};
}
struct TexturePreviewBinaryDitherCandidate
{
uint32_t mask { 0 };
std::array<float, 3> rgb { { 1.f, 1.f, 1.f } };
std::array<float, 3> oklab { { 1.f, 0.f, 0.f } };
};
std::vector<float> binary_component_visibility_weights_for_texture_preview(const TexturePreviewSimulationSettings &settings,
uint32_t mask)
{
const size_t component_count = settings.component_colors.size();
std::vector<float> weights(component_count, 0.f);
for (size_t component_idx = 0; component_idx < component_count; ++component_idx) {
const float strength = component_idx < settings.component_strength_factors.size() ?
std::clamp(settings.component_strength_factors[component_idx], 0.f, 1.f) :
1.f;
const float minimum = component_idx < settings.component_minimum_offset_factors.size() ?
std::clamp(settings.component_minimum_offset_factors[component_idx], 0.f, 1.f) :
0.f;
const bool active = (mask & (uint32_t(1) << component_idx)) != 0;
weights[component_idx] = std::clamp(minimum + (active ? strength * (1.f - minimum) : 0.f), 0.f, 1.f);
}
return weights;
}
std::vector<TexturePreviewBinaryDitherCandidate> binary_dither_candidates_for_texture_preview(
const TexturePreviewSimulationSettings &settings)
{
std::vector<TexturePreviewBinaryDitherCandidate> candidates;
const size_t component_count = settings.component_colors.size();
if (component_count == 0 || component_count > 16)
return candidates;
const uint32_t mask_end = uint32_t(1) << component_count;
candidates.reserve(size_t(mask_end - 1));
for (uint32_t mask = 1; mask < mask_end; ++mask) {
TexturePreviewBinaryDitherCandidate candidate;
candidate.mask = mask;
std::vector<float> weights = binary_component_visibility_weights_for_texture_preview(settings, mask);
float total_weight = 0.f;
for (const float weight : weights)
total_weight += weight;
if (total_weight <= k_epsilon) {
for (size_t component_idx = 0; component_idx < component_count; ++component_idx)
if ((mask & (uint32_t(1) << component_idx)) != 0)
weights[component_idx] = 1.f;
}
candidate.rgb = mix_color_solver_components(settings.component_colors,
weights,
color_solver_mix_model_from_index(settings.generic_solver_mix_model));
candidate.oklab = oklab_from_srgb(candidate.rgb);
candidates.emplace_back(candidate);
}
return candidates;
}
std::array<float, 3> texture_preview_target_oklab(const TexturePreviewSimulationSettings &settings,
const std::array<float, 4> &sample_rgba)
{
std::array<float, 3> target = {
clamp01(sample_rgba[0]),
clamp01(sample_rgba[1]),
clamp01(sample_rgba[2])
};
if (std::abs(settings.tone_gamma - 1.f) > 1e-5f) {
target[0] = apply_texture_tone_gamma(target[0], settings.tone_gamma);
target[1] = apply_texture_tone_gamma(target[1], settings.tone_gamma);
target[2] = apply_texture_tone_gamma(target[2], settings.tone_gamma);
}
target = apply_texture_contrast_to_rgb(target, std::clamp(settings.contrast_pct, 25.f, 300.f) / 100.f);
return oklab_from_srgb(target);
}
struct TexturePreviewBinaryDitherNearestResult {
size_t best_idx { size_t(-1) };
size_t second_idx { size_t(-1) };
float best_error { std::numeric_limits<float>::max() };
float second_error { std::numeric_limits<float>::max() };
};
TexturePreviewBinaryDitherNearestResult nearest_binary_dither_candidates_for_texture_preview(
const std::vector<TexturePreviewBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab)
{
TexturePreviewBinaryDitherNearestResult result;
const std::array<float, 3> axis_weights = generic_solver_v2_axis_weights(target_oklab);
for (size_t idx = 0; idx < candidates.size(); ++idx) {
const std::array<float, 3> &candidate = candidates[idx].oklab;
const float dl = candidate[0] - target_oklab[0];
const float da = candidate[1] - target_oklab[1];
const float db = candidate[2] - target_oklab[2];
const float error = axis_weights[0] * dl * dl + axis_weights[1] * da * da + axis_weights[2] * db * db;
if (error < result.best_error) {
result.second_error = result.best_error;
result.second_idx = result.best_idx;
result.best_error = error;
result.best_idx = idx;
} else if (error < result.second_error) {
result.second_error = error;
result.second_idx = idx;
}
}
return result;
}
size_t nearest_binary_dither_candidate_for_texture_preview(const std::vector<TexturePreviewBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab)
{
return nearest_binary_dither_candidates_for_texture_preview(candidates, target_oklab).best_idx;
}
float binary_dither_alternate_fraction_for_texture_preview(const std::vector<TexturePreviewBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab,
size_t base_idx,
size_t alternate_idx)
{
if (base_idx >= candidates.size() || alternate_idx >= candidates.size() || base_idx == alternate_idx)
return 0.f;
const std::array<float, 3> axis_weights = generic_solver_v2_axis_weights(target_oklab);
const std::array<float, 3> &base = candidates[base_idx].oklab;
const std::array<float, 3> &alternate = candidates[alternate_idx].oklab;
float numerator = 0.f;
float denominator = 0.f;
for (size_t axis = 0; axis < 3; ++axis) {
const float delta = alternate[axis] - base[axis];
numerator += axis_weights[axis] * (target_oklab[axis] - base[axis]) * delta;
denominator += axis_weights[axis] * delta * delta;
}
if (!std::isfinite(numerator) || !std::isfinite(denominator) || denominator <= 1e-12f)
return 0.f;
return std::clamp(numerator / denominator, 0.f, 1.f);
}
size_t thresholded_binary_dither_candidate_for_texture_preview(const std::vector<TexturePreviewBinaryDitherCandidate> &candidates,
const std::array<float, 3> &target_oklab,
float threshold)
{
const TexturePreviewBinaryDitherNearestResult nearest =
nearest_binary_dither_candidates_for_texture_preview(candidates, target_oklab);
if (nearest.best_idx >= candidates.size())
return size_t(-1);
if (nearest.second_idx >= candidates.size())
return nearest.best_idx;
const float alternate_fraction =
binary_dither_alternate_fraction_for_texture_preview(candidates, target_oklab, nearest.best_idx, nearest.second_idx);
return std::clamp(threshold, 0.f, 1.f) < alternate_fraction ? nearest.second_idx : nearest.best_idx;
}
float ordered_bayer_threshold_for_texture_preview(int x, int y)
{
static constexpr int matrix[8][8] = {
{ 0, 48, 12, 60, 3, 51, 15, 63 },
{ 32, 16, 44, 28, 35, 19, 47, 31 },
{ 8, 56, 4, 52, 11, 59, 7, 55 },
{ 40, 24, 36, 20, 43, 27, 39, 23 },
{ 2, 50, 14, 62, 1, 49, 13, 61 },
{ 34, 18, 46, 30, 33, 17, 45, 29 },
{ 10, 58, 6, 54, 9, 57, 5, 53 },
{ 42, 26, 38, 22, 41, 25, 37, 21 }
};
const int bx = ((x % 8) + 8) % 8;
const int by = ((y % 8) + 8) % 8;
return (float(matrix[by][bx]) + 0.5f) / 64.f - 0.5f;
}
float halftone_threshold_for_texture_preview(int x, int y, float dot_size_mm)
{
const float period_px =
std::clamp(std::clamp(dot_size_mm, 0.08f, 2.f) / TextureMappingZone::DefaultDitheringResolutionMm, 2.f, 64.f);
const float u = float(x) / period_px - std::floor(float(x) / period_px);
const float v = float(y) / period_px - std::floor(float(y) / period_px);
const float dx = u - 0.5f;
const float dy = v - 0.5f;
const float radius_area = (dx * dx + dy * dy) / 0.5f;
return std::clamp(radius_area, 0.f, 1.f) - 0.5f;
}
std::vector<float> optimized_primary_component_weights_for_target(const std::array<float, 3> &target_rgb,
size_t component_count,
int filament_color_mode,
@@ -1588,6 +1785,14 @@ std::vector<float> component_weights_for_texture_preview(const TexturePreviewSim
if (component_count == 0)
return {};
if (settings.dithering_enabled && settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues)) {
const std::vector<TexturePreviewBinaryDitherCandidate> candidates = binary_dither_candidates_for_texture_preview(settings);
const std::array<float, 3> target_oklab = texture_preview_target_oklab(settings, sample_rgba);
const size_t candidate_idx = nearest_binary_dither_candidate_for_texture_preview(candidates, target_oklab);
if (candidate_idx < candidates.size())
return binary_component_visibility_weights_for_texture_preview(settings, candidates[candidate_idx].mask);
}
std::array<float, 3> target = {
clamp01(sample_rgba[0]),
clamp01(sample_rgba[1]),
@@ -1750,7 +1955,15 @@ std::optional<TexturePreviewSimulationSettings> texture_preview_simulation_setti
int(TextureMappingZone::FilamentColorRGBKW));
settings.force_sequential_filaments = zone->force_sequential_filaments;
settings.limit_texture_resolution = zone->preview_limit_resolution;
settings.compact_offset_mode = zone->compact_offset_mode;
settings.dithering_enabled =
zone->dithering_enabled &&
settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues);
settings.dithering_method = std::clamp(zone->dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone));
settings.dithering_resolution_mm = std::clamp(zone->dithering_resolution_mm, 0.04f, 0.25f);
settings.halftone_dot_size_mm = std::clamp(zone->halftone_dot_size_mm, 0.08f, 2.f);
settings.compact_offset_mode = zone->compact_offset_mode || settings.dithering_enabled;
settings.use_legacy_fixed_color_mode = zone->use_legacy_fixed_color_mode;
settings.contrast_pct = std::clamp(zone->contrast_pct, 25.f, 300.f);
settings.tone_gamma = (!std::isfinite(zone->tone_gamma) || zone->tone_gamma <= 0.f) ?
@@ -1825,6 +2038,12 @@ size_t texture_preview_simulation_signature(const ModelVolume &model_volume,
mix(std::hash<int>{}(settings.limit_texture_resolution ? 1 : 0));
mix(std::hash<int>{}(settings.compact_offset_mode ? 1 : 0));
mix(std::hash<int>{}(settings.use_legacy_fixed_color_mode ? 1 : 0));
mix(std::hash<int>{}(settings.dithering_enabled ? 1 : 0));
mix(std::hash<int>{}(settings.dithering_method));
if (settings.dithering_method == int(TextureMappingZone::DitheringHalftone))
mix(std::hash<int>{}(int(std::lround(settings.halftone_dot_size_mm * 1000.f))));
else
mix(std::hash<int>{}(int(std::lround(settings.dithering_resolution_mm * 1000.f))));
mix(std::hash<int>{}(settings.generic_solver_lookup_mode));
mix(std::hash<int>{}(settings.generic_solver_mode));
mix(std::hash<int>{}(settings.generic_solver_mix_model));
@@ -1876,9 +2095,18 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig
const bool use_raw_offsets =
source_raw_component_channels.size() == settings.component_colors.size() &&
source_raw_offsets.size() >= size_t(width) * size_t(height) * size_t(source_raw_channels);
const bool use_binary_dithering =
settings.dithering_enabled &&
settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues) &&
!use_raw_offsets;
const std::vector<TexturePreviewBinaryDitherCandidate> binary_dither_candidates =
use_binary_dithering ? binary_dither_candidates_for_texture_preview(settings) :
std::vector<TexturePreviewBinaryDitherCandidate>{};
std::unordered_map<unsigned int, std::array<unsigned char, 4>> simulated_color_cache;
simulated_color_cache.reserve(std::min(size_t(result.width) * size_t(result.height), size_t(65536)));
std::vector<std::array<float, 3>> floyd_error_current(result.width, { { 0.f, 0.f, 0.f } });
std::vector<std::array<float, 3>> floyd_error_next(result.width, { { 0.f, 0.f, 0.f } });
for (unsigned int y = 0; y < result.height; ++y) {
for (unsigned int x = 0; x < result.width; ++x) {
@@ -1900,8 +2128,8 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig
texture_preview_rgb_cache_key(source_rgb);
const size_t idx = (size_t(y) * size_t(result.width) + size_t(x)) * 4;
auto cached_color = !use_raw_offsets ? simulated_color_cache.find(cache_key) : simulated_color_cache.end();
if (!use_raw_offsets && cached_color != simulated_color_cache.end()) {
auto cached_color = !use_raw_offsets && !use_binary_dithering ? simulated_color_cache.find(cache_key) : simulated_color_cache.end();
if (!use_raw_offsets && !use_binary_dithering && cached_color != simulated_color_cache.end()) {
result.rgba[idx + 0] = cached_color->second[0];
result.rgba[idx + 1] = cached_color->second[1];
result.rgba[idx + 2] = cached_color->second[2];
@@ -1930,7 +2158,57 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig
if (component_weights.size() == settings.component_colors.size())
component_weights = raw_offset_print_width_weights_for_texture_preview(settings, component_weights);
} else {
component_weights = component_weights_for_texture_preview(settings, sample_rgba);
if (use_binary_dithering && !binary_dither_candidates.empty()) {
std::array<float, 3> target_oklab = texture_preview_target_oklab(settings, sample_rgba);
const int clamped_method = std::clamp(settings.dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone));
if (clamped_method == int(TextureMappingZone::DitheringFloydSteinberg)) {
for (size_t axis = 0; axis < 3; ++axis)
target_oklab[axis] += floyd_error_current[x][axis];
}
bool thresholded_dither = false;
float threshold = 0.f;
if (clamped_method == int(TextureMappingZone::DitheringOrderedBayer)) {
thresholded_dither = true;
threshold = ordered_bayer_threshold_for_texture_preview(int(x), int(y)) + 0.5f;
} else if (clamped_method == int(TextureMappingZone::DitheringHalftone)) {
thresholded_dither = true;
threshold = halftone_threshold_for_texture_preview(int(x), int(y), settings.halftone_dot_size_mm) + 0.5f;
}
const size_t candidate_idx =
thresholded_dither ?
thresholded_binary_dither_candidate_for_texture_preview(binary_dither_candidates, target_oklab, threshold) :
nearest_binary_dither_candidate_for_texture_preview(binary_dither_candidates, target_oklab);
if (candidate_idx < binary_dither_candidates.size()) {
const TexturePreviewBinaryDitherCandidate &candidate = binary_dither_candidates[candidate_idx];
component_weights = binary_component_visibility_weights_for_texture_preview(settings, candidate.mask);
if (clamped_method == int(TextureMappingZone::DitheringFloydSteinberg)) {
const std::array<float, 3> error = {
target_oklab[0] - candidate.oklab[0],
target_oklab[1] - candidate.oklab[1],
target_oklab[2] - candidate.oklab[2]
};
auto add_error = [&error](std::array<float, 3> &dst, float factor) {
for (size_t axis = 0; axis < 3; ++axis)
dst[axis] += error[axis] * factor;
};
if (x + 1 < result.width)
add_error(floyd_error_current[x + 1], 7.f / 16.f);
if (y + 1 < result.height) {
if (x > 0)
add_error(floyd_error_next[x - 1], 3.f / 16.f);
add_error(floyd_error_next[x], 5.f / 16.f);
if (x + 1 < result.width)
add_error(floyd_error_next[x + 1], 1.f / 16.f);
}
}
}
} else {
component_weights = component_weights_for_texture_preview(settings, sample_rgba);
}
}
float activity = 0.f;
for (const float weight : component_weights)
@@ -1955,13 +2233,20 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig
to_u8(simulated_rgb[2]),
255
};
if (!use_raw_offsets)
if (!use_raw_offsets && !use_binary_dithering)
simulated_color_cache.emplace(cache_key, out_rgba);
result.rgba[idx + 0] = out_rgba[0];
result.rgba[idx + 1] = out_rgba[1];
result.rgba[idx + 2] = out_rgba[2];
result.rgba[idx + 3] = out_rgba[3];
}
if (use_binary_dithering &&
std::clamp(settings.dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone)) == int(TextureMappingZone::DitheringFloydSteinberg)) {
floyd_error_current.swap(floyd_error_next);
std::fill(floyd_error_next.begin(), floyd_error_next.end(), std::array<float, 3>{ { 0.f, 0.f, 0.f } });
}
}
return result;
@@ -3911,6 +4196,12 @@ size_t texture_preview_settings_signature(size_t num_physical, const TextureMapp
signature_mix(std::hash<int>{}(zone.nonlinear_offset_adjustment ? 1 : 0));
signature_mix(std::hash<int>{}(zone.compact_offset_mode ? 1 : 0));
signature_mix(std::hash<int>{}(zone.use_legacy_fixed_color_mode ? 1 : 0));
signature_mix(std::hash<int>{}(zone.dithering_enabled ? 1 : 0));
signature_mix(std::hash<int>{}(zone.dithering_method));
if (zone.dithering_method == int(TextureMappingZone::DitheringHalftone))
signature_mix_float(zone.halftone_dot_size_mm, 1000.f);
else
signature_mix_float(zone.dithering_resolution_mm, 1000.f);
signature_mix(std::hash<int>{}(zone.minimum_visibility_offset_enabled ? 1 : 0));
signature_mix_float(zone.minimum_visibility_offset_pct, 100.f);
signature_mix(std::hash<int>{}(zone.generic_solver_lookup_mode));

View File

@@ -1084,6 +1084,10 @@ public:
int generic_solver_lookup_mode,
int generic_solver_mode,
int generic_solver_mix_model,
bool dithering_enabled,
int dithering_method,
float dithering_resolution_mm,
float halftone_dot_size_mm,
const TextureMappingManager &texture_mapping_zones,
const TextureMappingGlobalSettings &global_settings,
const TextureMappingPrimeTowerImage &prime_tower_image,
@@ -1308,6 +1312,71 @@ public:
m_compact_offset_mode_checkbox->SetToolTip(
_L("Normalizes sampled filament offsets so the strongest active color uses the full maximum line width."));
experimental_box->Add(m_compact_offset_mode_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap);
auto *dithering_row = new wxBoxSizer(wxHORIZONTAL);
m_dithering_enabled_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Enable dithering"));
m_dithering_enabled_checkbox->SetValue(dithering_enabled);
dithering_row->Add(m_dithering_enabled_checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
wxArrayString dithering_choices;
dithering_choices.Add(_L("Closest combination (no dithering)"));
dithering_choices.Add(_L("Floyd-Steinberg"));
dithering_choices.Add(_L("Ordered Bayer"));
dithering_choices.Add(_L("Halftone"));
m_dithering_method_choice = new wxChoice(experimental_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, dithering_choices);
m_dithering_method_choice->SetSelection(std::clamp(dithering_method,
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone)));
dithering_row->Add(m_dithering_method_choice, 1, wxALIGN_CENTER_VERTICAL);
m_dithering_enabled_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) {
update_dithering_options_visibility(true);
});
m_dithering_method_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent &) {
update_dithering_options_visibility(true);
});
experimental_box->Add(dithering_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap);
m_dithering_resolution_panel = new wxPanel(experimental_page, wxID_ANY);
auto *dithering_resolution_row = new wxBoxSizer(wxHORIZONTAL);
m_dithering_resolution_panel->SetSizer(dithering_resolution_row);
dithering_resolution_row->Add(new wxStaticText(m_dithering_resolution_panel, wxID_ANY, _L("Horizontal resolution")),
0,
wxALIGN_CENTER_VERTICAL | wxRIGHT,
gap);
m_dithering_resolution_spin =
new wxSpinCtrlDouble(m_dithering_resolution_panel,
wxID_ANY,
wxEmptyString,
wxDefaultPosition,
wxSize(FromDIP(84), -1),
wxSP_ARROW_KEYS | wxALIGN_RIGHT,
0.04,
0.25,
std::clamp(double(dithering_resolution_mm), 0.04, 0.25),
0.01);
m_dithering_resolution_spin->SetDigits(2);
dithering_resolution_row->Add(m_dithering_resolution_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2);
dithering_resolution_row->Add(new wxStaticText(m_dithering_resolution_panel, wxID_ANY, _L("mm")), 0, wxALIGN_CENTER_VERTICAL);
experimental_box->Add(m_dithering_resolution_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap);
m_halftone_dot_size_panel = new wxPanel(experimental_page, wxID_ANY);
auto *halftone_dot_size_row = new wxBoxSizer(wxHORIZONTAL);
m_halftone_dot_size_panel->SetSizer(halftone_dot_size_row);
halftone_dot_size_row->Add(new wxStaticText(m_halftone_dot_size_panel, wxID_ANY, _L("Halftone dot size")),
0,
wxALIGN_CENTER_VERTICAL | wxRIGHT,
gap);
m_halftone_dot_size_spin =
new wxSpinCtrlDouble(m_halftone_dot_size_panel,
wxID_ANY,
wxEmptyString,
wxDefaultPosition,
wxSize(FromDIP(84), -1),
wxSP_ARROW_KEYS | wxALIGN_RIGHT,
0.08,
2.0,
std::clamp(double(halftone_dot_size_mm), 0.08, 2.0),
0.01);
m_halftone_dot_size_spin->SetDigits(2);
halftone_dot_size_row->Add(m_halftone_dot_size_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2);
halftone_dot_size_row->Add(new wxStaticText(m_halftone_dot_size_panel, wxID_ANY, _L("mm")), 0, wxALIGN_CENTER_VERTICAL);
experimental_box->Add(m_halftone_dot_size_panel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap);
m_use_legacy_fixed_color_mode_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Use legacy fixed color mode"));
m_use_legacy_fixed_color_mode_checkbox->SetValue(use_legacy_fixed_color_mode);
experimental_box->Add(m_use_legacy_fixed_color_mode_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap);
@@ -1371,6 +1440,7 @@ public:
update_minimum_visibility_offset_visibility(true);
});
experimental_box->Add(minimum_visibility_offset_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap);
update_dithering_options_visibility(false);
update_minimum_visibility_offset_visibility(false);
experimental_root->Add(experimental_box, 0, wxEXPAND | wxALL, gap);
@@ -1551,9 +1621,34 @@ 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 compact_offset_mode() const { return m_compact_offset_mode_checkbox && m_compact_offset_mode_checkbox->GetValue(); }
bool compact_offset_mode() const { return dithering_enabled() || (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(); }
bool dithering_enabled() const { return m_dithering_enabled_checkbox && m_dithering_enabled_checkbox->GetValue(); }
int dithering_method() const
{
return m_dithering_method_choice ?
std::clamp(m_dithering_method_choice->GetSelection(),
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone)) :
TextureMappingZone::DefaultDitheringMethod;
}
float dithering_resolution_mm() const
{
return float(std::clamp(m_dithering_resolution_spin != nullptr ?
m_dithering_resolution_spin->GetValue() :
double(TextureMappingZone::DefaultDitheringResolutionMm),
0.04,
0.25));
}
float halftone_dot_size_mm() const
{
return float(std::clamp(m_halftone_dot_size_spin != nullptr ?
m_halftone_dot_size_spin->GetValue() :
double(TextureMappingZone::DefaultHalftoneDotSizeMm),
0.08,
2.0));
}
bool minimum_visibility_offset_enabled() const
{
return m_minimum_visibility_offset_checkbox && m_minimum_visibility_offset_checkbox->GetValue();
@@ -1842,6 +1937,35 @@ private:
}
}
void update_dithering_options_visibility(bool fit_dialog)
{
const bool enabled = m_dithering_enabled_checkbox != nullptr && m_dithering_enabled_checkbox->GetValue();
const int method = m_dithering_method_choice != nullptr ?
std::clamp(m_dithering_method_choice->GetSelection(),
int(TextureMappingZone::DitheringClosest),
int(TextureMappingZone::DitheringHalftone)) :
TextureMappingZone::DefaultDitheringMethod;
const bool halftone = enabled && method == int(TextureMappingZone::DitheringHalftone);
if (m_dithering_method_choice != nullptr)
m_dithering_method_choice->Enable(enabled);
if (m_dithering_resolution_panel != nullptr)
m_dithering_resolution_panel->Show(enabled && !halftone);
if (m_halftone_dot_size_panel != nullptr)
m_halftone_dot_size_panel->Show(halftone);
if (m_compact_offset_mode_checkbox != nullptr) {
if (enabled)
m_compact_offset_mode_checkbox->SetValue(true);
m_compact_offset_mode_checkbox->Enable(!enabled);
}
if (!fit_dialog)
return;
update_options_book_min_size();
if (GetSizer() != nullptr) {
Layout();
Fit();
}
}
wxChoice *m_options_tab_choice {nullptr};
wxSimplebook *m_options_book {nullptr};
wxChoice *m_texture_mapping_mode_choice {nullptr};
@@ -1858,6 +1982,12 @@ private:
wxCheckBox *m_seam_hiding_checkbox {nullptr};
wxCheckBox *m_nonlinear_offset_adjustment_checkbox {nullptr};
wxCheckBox *m_compact_offset_mode_checkbox {nullptr};
wxCheckBox *m_dithering_enabled_checkbox {nullptr};
wxChoice *m_dithering_method_choice {nullptr};
wxPanel *m_dithering_resolution_panel {nullptr};
wxSpinCtrlDouble *m_dithering_resolution_spin {nullptr};
wxPanel *m_halftone_dot_size_panel {nullptr};
wxSpinCtrlDouble *m_halftone_dot_size_spin {nullptr};
wxCheckBox *m_use_legacy_fixed_color_mode_checkbox {nullptr};
wxCheckBox *m_high_speed_image_texture_sampling_checkbox {nullptr};
wxCheckBox *m_minimum_visibility_offset_checkbox {nullptr};
@@ -5972,6 +6102,10 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager)
updated.generic_solver_lookup_mode,
updated.generic_solver_mode,
updated.generic_solver_mix_model,
updated.dithering_enabled,
updated.dithering_method,
updated.dithering_resolution_mm,
updated.halftone_dot_size_mm,
bundle->texture_mapping_zones,
bundle->texture_mapping_global_settings,
wxGetApp().model().texture_mapping_prime_tower_image,
@@ -6001,6 +6135,12 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager)
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();
updated.dithering_enabled = dlg.dithering_enabled();
updated.dithering_method = dlg.dithering_method();
updated.dithering_resolution_mm = dlg.dithering_resolution_mm();
updated.halftone_dot_size_mm = dlg.halftone_dot_size_mm();
if (updated.dithering_enabled)
updated.compact_offset_mode = true;
updated.minimum_visibility_offset_enabled = dlg.minimum_visibility_offset_enabled();
updated.minimum_visibility_offset_pct = dlg.minimum_visibility_offset_pct();
updated.generic_solver_lookup_mode = dlg.generic_solver_lookup_mode();