From b7b00ede5579b6c5d8e3e01e156568632b3c918e Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 22 May 2025 02:47:06 -0400 Subject: [PATCH 1/4] New Autocolorize mode (under test). --- src/libslic3r/CMakeLists.txt | 2 + .../MultiMaterialAutoColorization.cpp | 535 ++++++++++++++++++ .../MultiMaterialAutoColorization.hpp | 81 +++ .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 248 ++++++++ .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 14 + 5 files changed, 880 insertions(+) create mode 100644 src/libslic3r/MultiMaterialAutoColorization.cpp create mode 100644 src/libslic3r/MultiMaterialAutoColorization.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index f7773673ae6..0863b4d529c 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -276,6 +276,8 @@ set(SLIC3R_SOURCES FileReader.hpp MultiMaterialSegmentation.cpp MultiMaterialSegmentation.hpp + MultiMaterialAutoColorization.cpp + MultiMaterialAutoColorization.hpp MeshNormals.hpp MeshNormals.cpp Measure.hpp diff --git a/src/libslic3r/MultiMaterialAutoColorization.cpp b/src/libslic3r/MultiMaterialAutoColorization.cpp new file mode 100644 index 00000000000..5f0039f8067 --- /dev/null +++ b/src/libslic3r/MultiMaterialAutoColorization.cpp @@ -0,0 +1,535 @@ +///|/ Copyright (c) Prusa Research 2025 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "MultiMaterialAutoColorization.hpp" + +#include +#include +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleSelector.hpp" +#include "libslic3r/TriangleMesh.hpp" + +namespace Slic3r { + +// Perlin noise implementation for the noise pattern +namespace { + class PerlinNoise { + private: + std::vector p; + + public: + PerlinNoise(int seed = 0) { + // Initialize the permutation vector with the reference values + p = { + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, + 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, + 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, + 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 + }; + + // Duplicate the permutation vector + p.insert(p.end(), p.begin(), p.end()); + + // If a seed is provided, shuffle the permutation vector + if (seed > 0) { + std::mt19937 rng(seed); + std::shuffle(p.begin(), p.end(), rng); + } + } + + double noise(double x, double y, double z) { + // Find the unit cube that contains the point + int X = static_cast(std::floor(x)) & 255; + int Y = static_cast(std::floor(y)) & 255; + int Z = static_cast(std::floor(z)) & 255; + + // Find relative x, y, z of point in cube + x -= std::floor(x); + y -= std::floor(y); + z -= std::floor(z); + + // Compute fade curves for each of x, y, z + double u = fade(x); + double v = fade(y); + double w = fade(z); + + // Hash coordinates of the 8 cube corners + int A = p[X] + Y; + int AA = p[A] + Z; + int AB = p[A + 1] + Z; + int B = p[X + 1] + Y; + int BA = p[B] + Z; + int BB = p[B + 1] + Z; + + // Add blended results from 8 corners of cube + double res = lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z), + grad(p[BA], x-1, y, z)), + lerp(u, grad(p[AB], x, y-1, z), + grad(p[BB], x-1, y-1, z))), + lerp(v, lerp(u, grad(p[AA+1], x, y, z-1), + grad(p[BA+1], x-1, y, z-1)), + lerp(u, grad(p[AB+1], x, y-1, z-1), + grad(p[BB+1], x-1, y-1, z-1)))); + return (res + 1.0) / 2.0; + } + + private: + double fade(double t) { + return t * t * t * (t * (t * 6 - 15) + 10); + } + + double lerp(double t, double a, double b) { + return a + t * (b - a); + } + + double grad(int hash, double x, double y, double z) { + int h = hash & 15; + // Convert lower 4 bits of hash into 12 gradient directions + double u = h < 8 ? x : y, + v = h < 4 ? y : h == 12 || h == 14 ? x : z; + return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); + } + }; +} + +// Helper function to assign a color (extruder) based on a normalized value and distribution +int assign_color_from_distribution(float normalized_value, const std::vector& extruders, const std::vector& distribution) { + if (extruders.empty() || distribution.empty()) + return 0; + + // Calculate cumulative distribution + std::vector cumulative_dist; + float sum = 0.0f; + for (float d : distribution) { + if (d > 0.0f) { + sum += d; + cumulative_dist.push_back(sum); + } + } + + // Normalize cumulative distribution + if (sum > 0.0f) { + for (float& d : cumulative_dist) + d /= sum; + } else { + return extruders[0]; // Default to first extruder if all distributions are 0 + } + + // Find the appropriate color based on the normalized value + for (size_t i = 0; i < cumulative_dist.size(); ++i) { + if (normalized_value <= cumulative_dist[i] && extruders[i] > 0) + return extruders[i]; + } + + // Default to the last valid extruder + for (int i = static_cast(extruders.size()) - 1; i >= 0; --i) { + if (extruders[i] > 0) + return extruders[i]; + } + + return 0; // Fallback +} + +// Apply height gradient colorization +void apply_height_gradient(TriangleSelector& selector, const ModelVolume& volume, const MMUAutoColorizationParams& params) { + const TriangleMesh& mesh = volume.mesh(); + const Transform3d& volume_transform = volume.get_matrix(); + + // Get the bounding box to determine height range + BoundingBoxf3 bbox = mesh.bounding_box().transformed(volume_transform); + float min_z = bbox.min.z(); + float max_z = bbox.max.z(); + float height_range = max_z - min_z; + + // Calculate start and end heights based on percentages + float start_height = min_z + (params.height_start_percent / 100.0f) * height_range; + float end_height = min_z + (params.height_end_percent / 100.0f) * height_range; + + // Process each triangle + for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; + + // Calculate the center of the triangle + Vec3f center = Vec3f::Zero(); + for (int j = 0; j < 3; ++j) { + Vec3f vertex = mesh.its.vertices[indices[j]].cast(); + center += vertex; + } + center /= 3.0f; + + // Transform the center to world coordinates + Vec3d center_world = volume_transform * center.cast(); + + // Calculate normalized height position + float normalized_height = (center_world.z() - start_height) / (end_height - start_height); + normalized_height = std::clamp(normalized_height, 0.0f, 1.0f); + + // Reverse direction if needed + if (params.height_reverse) + normalized_height = 1.0f - normalized_height; + + // Assign color based on distribution + int extruder_id = assign_color_from_distribution(normalized_height, params.extruders, params.distribution); + + // Set the triangle state + if (extruder_id > 0) + selector.set_facet(i, TriangleStateType(extruder_id)); + } +} + +// Apply radial gradient colorization +void apply_radial_gradient(TriangleSelector& selector, const ModelVolume& volume, const MMUAutoColorizationParams& params) { + const TriangleMesh& mesh = volume.mesh(); + const Transform3d& volume_transform = volume.get_matrix(); + + // Process each triangle + for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; + + // Calculate the center of the triangle + Vec3f center = Vec3f::Zero(); + for (int j = 0; j < 3; ++j) { + Vec3f vertex = mesh.its.vertices[indices[j]].cast(); + center += vertex; + } + center /= 3.0f; + + // Transform the center to world coordinates + Vec3d center_world = volume_transform * center.cast(); + + // Calculate distance from radial center (in XY plane) + Vec3f radial_center_world = params.radial_center; + float distance = std::sqrt(std::pow(center_world.x() - radial_center_world.x(), 2) + + std::pow(center_world.y() - radial_center_world.y(), 2)); + + // Normalize distance by radius + float normalized_distance = distance / params.radial_radius; + normalized_distance = std::clamp(normalized_distance, 0.0f, 1.0f); + + // Reverse direction if needed + if (params.radial_reverse) + normalized_distance = 1.0f - normalized_distance; + + // Assign color based on distribution + int extruder_id = assign_color_from_distribution(normalized_distance, params.extruders, params.distribution); + + // Set the triangle state + if (extruder_id > 0) + selector.set_facet(i, TriangleStateType(extruder_id)); + } +} + +// Apply spiral pattern colorization +void apply_spiral_pattern(TriangleSelector& selector, const ModelVolume& volume, const MMUAutoColorizationParams& params) { + const TriangleMesh& mesh = volume.mesh(); + const Transform3d& volume_transform = volume.get_matrix(); + + // Process each triangle + for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; + + // Calculate the center of the triangle + Vec3f center = Vec3f::Zero(); + for (int j = 0; j < 3; ++j) { + Vec3f vertex = mesh.its.vertices[indices[j]].cast(); + center += vertex; + } + center /= 3.0f; + + // Transform the center to world coordinates + Vec3d center_world = volume_transform * center.cast(); + + // Calculate polar coordinates + Vec3f spiral_center_world = params.spiral_center; + float dx = center_world.x() - spiral_center_world.x(); + float dy = center_world.y() - spiral_center_world.y(); + float angle = std::atan2(dy, dx); + if (angle < 0) angle += 2 * M_PI; + + // Calculate distance from spiral center + float distance = std::sqrt(dx*dx + dy*dy); + + // Calculate spiral value (combination of angle and distance) + float spiral_value = (angle / (2 * M_PI) + distance / params.spiral_pitch) / params.spiral_turns; + spiral_value = std::fmod(spiral_value, 1.0f); + + // Reverse direction if needed + if (params.spiral_reverse) + spiral_value = 1.0f - spiral_value; + + // Assign color based on distribution + int extruder_id = assign_color_from_distribution(spiral_value, params.extruders, params.distribution); + + // Set the triangle state + if (extruder_id > 0) + selector.set_facet(i, TriangleStateType(extruder_id)); + } +} + +// Apply noise pattern colorization +void apply_noise_pattern(TriangleSelector& selector, const ModelVolume& volume, const MMUAutoColorizationParams& params) { + const TriangleMesh& mesh = volume.mesh(); + const Transform3d& volume_transform = volume.get_matrix(); + + // Initialize Perlin noise generator + PerlinNoise noise(params.noise_seed); + + // Process each triangle + for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; + + // Calculate the center of the triangle + Vec3f center = Vec3f::Zero(); + for (int j = 0; j < 3; ++j) { + Vec3f vertex = mesh.its.vertices[indices[j]].cast(); + center += vertex; + } + center /= 3.0f; + + // Transform the center to world coordinates + Vec3d center_world = volume_transform * center.cast(); + + // Calculate noise value + float scale = params.noise_scale / 100.0f; // Convert to a reasonable scale + float noise_value = noise.noise( + center_world.x() * scale, + center_world.y() * scale, + center_world.z() * scale + ); + + // Assign color based on noise value and distribution + int extruder_id = assign_color_from_distribution(noise_value, params.extruders, params.distribution); + + // Set the triangle state + if (extruder_id > 0) + selector.set_facet(i, TriangleStateType(extruder_id)); + } +} + +// Apply optimized color changes pattern +void apply_optimized_changes(TriangleSelector& selector, const ModelVolume& volume, const MMUAutoColorizationParams& params) { + // This is a simplified implementation that divides the model into horizontal bands + // A more sophisticated implementation would use clustering algorithms to minimize color changes + + const TriangleMesh& mesh = volume.mesh(); + const Transform3d& volume_transform = volume.get_matrix(); + + // Get the bounding box to determine height range + BoundingBoxf3 bbox = mesh.bounding_box().transformed(volume_transform); + float min_z = bbox.min.z(); + float max_z = bbox.max.z(); + float height_range = max_z - min_z; + + // Count active extruders + int active_extruders = 0; + for (int e : params.extruders) { + if (e > 0) active_extruders++; + } + + if (active_extruders == 0) return; + + // Calculate band height based on distribution + float band_height = height_range / active_extruders; + + // Process each triangle + for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; + + // Calculate the center of the triangle + Vec3f center = Vec3f::Zero(); + for (int j = 0; j < 3; ++j) { + Vec3f vertex = mesh.its.vertices[indices[j]].cast(); + center += vertex; + } + center /= 3.0f; + + // Transform the center to world coordinates + Vec3d center_world = volume_transform * center.cast(); + + // Calculate which band this triangle belongs to + int band = static_cast((center_world.z() - min_z) / band_height); + band = std::clamp(band, 0, active_extruders - 1); + + // Find the corresponding extruder + int extruder_idx = 0; + for (size_t e = 0; e < params.extruders.size(); ++e) { + if (params.extruders[e] > 0) { + if (extruder_idx == band) { + // Set the triangle state + selector.set_facet(i, TriangleStateType(params.extruders[e])); + break; + } + extruder_idx++; + } + } + } +} + +// Validate and normalize the auto-colorization parameters +MMUAutoColorizationParams validate_auto_colorization_params(const MMUAutoColorizationParams& params) { + MMUAutoColorizationParams validated = params; + + // Ensure we have at least one active extruder + bool has_active_extruder = false; + for (int e : validated.extruders) { + if (e > 0) { + has_active_extruder = true; + break; + } + } + + if (!has_active_extruder && !validated.extruders.empty()) { + validated.extruders[0] = 1; // Set first extruder as active if none are + } + + // Ensure distribution values are valid + float total_distribution = 0.0f; + for (float& d : validated.distribution) { + d = std::max(0.0f, d); // Ensure non-negative + total_distribution += d; + } + + // Normalize distribution if needed + if (total_distribution > 0.0f) { + for (float& d : validated.distribution) { + d = (d / total_distribution) * 100.0f; + } + } else if (!validated.distribution.empty()) { + // If all distributions are 0, set equal distribution for active extruders + int active_count = 0; + for (int e : validated.extruders) { + if (e > 0) active_count++; + } + + if (active_count > 0) { + float equal_value = 100.0f / active_count; + for (size_t i = 0; i < validated.extruders.size() && i < validated.distribution.size(); ++i) { + validated.distribution[i] = (validated.extruders[i] > 0) ? equal_value : 0.0f; + } + } + } + + // Ensure height gradient parameters are valid + validated.height_start_percent = std::clamp(validated.height_start_percent, 0.0f, 100.0f); + validated.height_end_percent = std::clamp(validated.height_end_percent, 0.0f, 100.0f); + + // Ensure radial gradient parameters are valid + validated.radial_radius = std::max(0.1f, validated.radial_radius); + + // Ensure spiral pattern parameters are valid + validated.spiral_pitch = std::max(0.1f, validated.spiral_pitch); + validated.spiral_turns = std::max(1, validated.spiral_turns); + + // Ensure noise pattern parameters are valid + validated.noise_scale = std::max(0.1f, validated.noise_scale); + validated.noise_threshold = std::clamp(validated.noise_threshold, 0.0f, 1.0f); + + return validated; +} + +// Apply automatic colorization to a model object +void apply_auto_colorization(ModelObject& model_object, const MMUAutoColorizationParams& params) { + // Validate and normalize parameters + MMUAutoColorizationParams validated_params = validate_auto_colorization_params(params); + + // Process each volume in the model object + for (ModelVolume* volume : model_object.volumes) { + if (!volume->is_model_part()) + continue; + + // Create a triangle selector for this volume + TriangleSelector selector(volume->mesh()); + + // Apply the selected pattern + switch (validated_params.pattern_type) { + case MMUAutoColorizationPattern::HeightGradient: + apply_height_gradient(selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::RadialGradient: + apply_radial_gradient(selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::SpiralPattern: + apply_spiral_pattern(selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::NoisePattern: + apply_noise_pattern(selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::OptimizedChanges: + apply_optimized_changes(selector, *volume, validated_params); + break; + default: + // Default to height gradient if unknown pattern type + apply_height_gradient(selector, *volume, validated_params); + break; + } + + // Apply the colorization to the volume + volume->mm_segmentation_facets.set(selector); + } +} + +// Generate a preview of the auto-colorization without modifying the model +std::vector> preview_auto_colorization( + const ModelObject& model_object, + const MMUAutoColorizationParams& params) +{ + // Validate and normalize parameters + MMUAutoColorizationParams validated_params = validate_auto_colorization_params(params); + + // Create a vector to store the triangle selectors + std::vector> selectors; + + // Process each volume in the model object + for (const ModelVolume* volume : model_object.volumes) { + if (!volume->is_model_part()) + continue; + + // Create a triangle selector for this volume + auto selector = std::make_unique(volume->mesh()); + + // Apply the selected pattern + switch (validated_params.pattern_type) { + case MMUAutoColorizationPattern::HeightGradient: + apply_height_gradient(*selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::RadialGradient: + apply_radial_gradient(*selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::SpiralPattern: + apply_spiral_pattern(*selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::NoisePattern: + apply_noise_pattern(*selector, *volume, validated_params); + break; + case MMUAutoColorizationPattern::OptimizedChanges: + apply_optimized_changes(*selector, *volume, validated_params); + break; + default: + // Default to height gradient if unknown pattern type + apply_height_gradient(*selector, *volume, validated_params); + break; + } + + // Add the selector to the vector + selectors.push_back(std::move(selector)); + } + + return selectors; +} + +} // namespace Slic3r diff --git a/src/libslic3r/MultiMaterialAutoColorization.hpp b/src/libslic3r/MultiMaterialAutoColorization.hpp new file mode 100644 index 00000000000..c251df4d29d --- /dev/null +++ b/src/libslic3r/MultiMaterialAutoColorization.hpp @@ -0,0 +1,81 @@ +///|/ Copyright (c) Prusa Research 2025 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_MultiMaterialAutoColorization_hpp_ +#define slic3r_MultiMaterialAutoColorization_hpp_ + +#include +#include +#include +#include + +#include "libslic3r/Point.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/TriangleSelector.hpp" + +namespace Slic3r { + +class ModelObject; +class ModelVolume; + +enum class MMUAutoColorizationPattern : int { + HeightGradient, + RadialGradient, + SpiralPattern, + NoisePattern, + OptimizedChanges, + Count +}; + +struct MMUAutoColorizationParams { + MMUAutoColorizationPattern pattern_type = MMUAutoColorizationPattern::HeightGradient; + + // Which extruders to use (1-based indices, 0 means not used) + std::vector extruders = {1, 2, 0, 0, 0}; + + // Percentage-based distribution for each extruder (0-100) + std::vector distribution = {50.0f, 50.0f, 0.0f, 0.0f, 0.0f}; + + // Pattern-specific parameters + + // For height gradient + float height_start_percent = 0.0f; // Start height as percentage of total height + float height_end_percent = 100.0f; // End height as percentage of total height + bool height_reverse = false; // Reverse the gradient direction + + // For radial gradient + Vec3f radial_center = Vec3f::Zero(); // Center point of the radial gradient + float radial_radius = 50.0f; // Radius of the gradient in mm + bool radial_reverse = false; // Reverse the gradient direction + + // For spiral pattern + Vec3f spiral_center = Vec3f::Zero(); // Center point of the spiral + float spiral_pitch = 10.0f; // Distance between spiral turns in mm + int spiral_turns = 5; // Number of complete turns + bool spiral_reverse = false; // Reverse the spiral direction + + // For noise pattern + float noise_scale = 10.0f; // Scale of the noise pattern + float noise_threshold = 0.5f; // Threshold for noise pattern + int noise_seed = 1234; // Seed for the noise generator + + // For optimized changes + int min_area_per_color = 100; // Minimum area (in mm²) per color to reduce color changes +}; + +// Apply automatic colorization to a model object based on the specified parameters +void apply_auto_colorization(ModelObject& model_object, const MMUAutoColorizationParams& params); + +// Generate a preview of the auto-colorization without modifying the model +// Returns a vector of triangle selectors with the colorization applied +std::vector> preview_auto_colorization( + const ModelObject& model_object, + const MMUAutoColorizationParams& params); + +// Helper function to validate and normalize the auto-colorization parameters +MMUAutoColorizationParams validate_auto_colorization_params(const MMUAutoColorizationParams& params); + +} // namespace Slic3r + +#endif // slic3r_MultiMaterialAutoColorization_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index abc840baf4d..ae49e5f33c2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -133,8 +133,42 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["split_triangles"] = _u8L("Split triangles"); m_desc["height_range_z_range"] = _u8L("Height range"); + + // Auto-colorization related descriptions + m_desc["auto_colorize"] = _u8L("Auto-colorize"); + m_desc["auto_colorize_enable"] = _u8L("Enable auto-colorization"); + m_desc["pattern_type"] = _u8L("Pattern type"); + m_desc["height_gradient"] = _u8L("Height gradient"); + m_desc["radial_gradient"] = _u8L("Radial gradient"); + m_desc["spiral_pattern"] = _u8L("Spiral pattern"); + m_desc["noise_pattern"] = _u8L("Noise pattern"); + m_desc["optimized_changes"] = _u8L("Optimized changes"); + m_desc["preview"] = _u8L("Preview"); + m_desc["apply"] = _u8L("Apply"); + m_desc["height_start"] = _u8L("Start height") + " %"; + m_desc["height_end"] = _u8L("End height") + " %"; + m_desc["height_reverse"] = _u8L("Reverse direction"); + m_desc["radial_radius"] = _u8L("Radius") + " " + _u8L("mm"); + m_desc["radial_reverse"] = _u8L("Reverse direction"); + m_desc["spiral_pitch"] = _u8L("Pitch") + " " + _u8L("mm"); + m_desc["spiral_turns"] = _u8L("Turns"); + m_desc["spiral_reverse"] = _u8L("Reverse direction"); + m_desc["noise_scale"] = _u8L("Scale"); + m_desc["noise_seed"] = _u8L("Seed"); + m_desc["extruder_use"] = _u8L("Use extruder"); + m_desc["distribution"] = _u8L("Distribution") + " %"; init_extruders_data(); + + // Initialize auto-colorization parameters + m_auto_colorize_params.extruders.resize(std::min(size_t(5), m_original_extruders_names.size())); + m_auto_colorize_params.distribution.resize(std::min(size_t(5), m_original_extruders_names.size())); + + // Set default values for extruders (first two enabled) + for (size_t i = 0; i < m_auto_colorize_params.extruders.size(); ++i) { + m_auto_colorize_params.extruders[i] = (i < 2) ? int(i + 1) : 0; + m_auto_colorize_params.distribution[i] = (i < 2) ? 50.0f : 0.0f; + } return true; } @@ -541,6 +575,13 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, from_u8(GUI::shortkey_ctrl_prefix()) + _L("Mouse wheel"))) m_c->object_clipper()->set_position_by_ratio(clp_dist, true); + ImGui::Separator(); + + // Add auto-colorization section + if (ImGuiPureWrap::collapsing_header(m_desc.at("auto_colorize"), ImGuiTreeNodeFlags_DefaultOpen)) { + render_auto_colorization_ui(x, y, bottom_limit, ImGui::GetContentRegionAvail().x); + } + ImGui::Separator(); if (ImGuiPureWrap::button(m_desc.at("remove_all"))) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset selection"), @@ -837,4 +878,211 @@ void GLMmSegmentationGizmo3DScene::finalize_triangle_indices() } } +// Preview auto-colorization without applying it +void GLGizmoMmuSegmentation::preview_auto_colorization() +{ + ModelObject* mo = m_c->selection_info()->model_object(); + if (!mo) + return; + + // Take a snapshot for undo/redo + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Preview auto-colorization"), + UndoRedo::SnapshotType::GizmoAction); + + // Generate a preview of the auto-colorization + auto preview_selectors = preview_auto_colorization(*mo, m_auto_colorize_params); + + // Apply the preview to the triangle selectors + int idx = -1; + for (ModelVolume* mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + + ++idx; + if (idx < int(preview_selectors.size())) { + // Copy the preview selector data to the actual selector + m_triangle_selectors[idx]->deserialize(preview_selectors[idx]->serialize(), false); + m_triangle_selectors[idx]->request_update_render_data(); + } + } + + // Mark the parent as dirty to trigger a redraw + m_parent.set_as_dirty(); +} + +// Apply auto-colorization to the model +void GLGizmoMmuSegmentation::apply_auto_colorization() +{ + ModelObject* mo = m_c->selection_info()->model_object(); + if (!mo) + return; + + // Take a snapshot for undo/redo + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Apply auto-colorization"), + UndoRedo::SnapshotType::GizmoAction); + + // Apply the auto-colorization to the model object + apply_auto_colorization(*mo, m_auto_colorize_params); + + // Update the triangle selectors from the model + update_from_model_object(); + + // Mark the parent as dirty to trigger a redraw and schedule background processing + m_parent.set_as_dirty(); + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +// Render the auto-colorization UI section +void GLGizmoMmuSegmentation::render_auto_colorization_ui(float x, float y, float bottom_limit, float window_width) +{ + const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + // Pattern type selection + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("pattern_type")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.7f); + + // Create a combo box for pattern selection + const char* pattern_items[] = { + m_desc.at("height_gradient").c_str(), + m_desc.at("radial_gradient").c_str(), + m_desc.at("spiral_pattern").c_str(), + m_desc.at("noise_pattern").c_str(), + m_desc.at("optimized_changes").c_str() + }; + + int current_pattern = static_cast(m_auto_colorize_params.pattern_type); + if (ImGui::Combo("##pattern_type", ¤t_pattern, pattern_items, IM_ARRAYSIZE(pattern_items))) { + m_auto_colorize_params.pattern_type = static_cast(current_pattern); + } + + ImGui::Separator(); + + // Extruder selection and distribution + for (size_t i = 0; i < m_auto_colorize_params.extruders.size(); ++i) { + ImGui::PushID(int(i)); + + // Checkbox to enable/disable this extruder + bool extruder_enabled = m_auto_colorize_params.extruders[i] > 0; + if (ImGuiPureWrap::checkbox(m_desc.at("extruder_use") + " " + std::to_string(i + 1), &extruder_enabled)) { + m_auto_colorize_params.extruders[i] = extruder_enabled ? int(i + 1) : 0; + } + + // Distribution slider (only show if extruder is enabled) + if (extruder_enabled) { + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + float distribution = m_auto_colorize_params.distribution[i]; + if (m_imgui->slider_float(("##distribution" + std::to_string(i)).c_str(), &distribution, 0.0f, 100.0f, "%.1f%%")) { + m_auto_colorize_params.distribution[i] = distribution; + } + } + + ImGui::PopID(); + } + + ImGui::Separator(); + + // Pattern-specific parameters + switch (m_auto_colorize_params.pattern_type) { + case MMUAutoColorizationPattern::HeightGradient: { + // Start height + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("height_start")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + m_imgui->slider_float("##height_start", &m_auto_colorize_params.height_start_percent, 0.0f, 100.0f, "%.1f%%"); + + // End height + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("height_end")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + m_imgui->slider_float("##height_end", &m_auto_colorize_params.height_end_percent, 0.0f, 100.0f, "%.1f%%"); + + // Reverse direction + ImGuiPureWrap::checkbox(m_desc.at("height_reverse"), &m_auto_colorize_params.height_reverse); + break; + } + + case MMUAutoColorizationPattern::RadialGradient: { + // Radius + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("radial_radius")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + m_imgui->slider_float("##radial_radius", &m_auto_colorize_params.radial_radius, 1.0f, 200.0f, "%.1f"); + + // Reverse direction + ImGuiPureWrap::checkbox(m_desc.at("radial_reverse"), &m_auto_colorize_params.radial_reverse); + break; + } + + case MMUAutoColorizationPattern::SpiralPattern: { + // Pitch + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("spiral_pitch")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + m_imgui->slider_float("##spiral_pitch", &m_auto_colorize_params.spiral_pitch, 1.0f, 50.0f, "%.1f"); + + // Turns + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("spiral_turns")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + int turns = m_auto_colorize_params.spiral_turns; + if (ImGui::SliderInt("##spiral_turns", &turns, 1, 20)) { + m_auto_colorize_params.spiral_turns = turns; + } + + // Reverse direction + ImGuiPureWrap::checkbox(m_desc.at("spiral_reverse"), &m_auto_colorize_params.spiral_reverse); + break; + } + + case MMUAutoColorizationPattern::NoisePattern: { + // Scale + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("noise_scale")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + m_imgui->slider_float("##noise_scale", &m_auto_colorize_params.noise_scale, 1.0f, 50.0f, "%.1f"); + + // Seed + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("noise_seed")); + ImGui::SameLine(); + ImGui::PushItemWidth(window_width * 0.5f); + int seed = m_auto_colorize_params.noise_seed; + if (ImGui::SliderInt("##noise_seed", &seed, 1, 10000)) { + m_auto_colorize_params.noise_seed = seed; + } + break; + } + + case MMUAutoColorizationPattern::OptimizedChanges: + // No specific parameters for optimized changes + ImGuiPureWrap::text(_u8L("Optimizes color changes to minimize tool changes.")); + break; + + default: + break; + } + + ImGui::Separator(); + + // Preview and Apply buttons + if (ImGuiPureWrap::button(m_desc.at("preview"), ImVec2(window_width * 0.48f, 0))) { + preview_auto_colorization(); + } + + ImGui::SameLine(); + + if (ImGuiPureWrap::button(m_desc.at("apply"), ImVec2(window_width * 0.48f, 0))) { + apply_auto_colorization(); + } +} + } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index a8e51962ddf..fb80ab01e62 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -8,6 +8,7 @@ #include "GLGizmoPainterBase.hpp" #include "slic3r/GUI/I18N.hpp" +#include "libslic3r/MultiMaterialAutoColorization.hpp" namespace Slic3r::GUI { @@ -150,6 +151,19 @@ class GLGizmoMmuSegmentation : public GLGizmoPainterBase // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. std::map m_desc; + + // Auto-colorization related members + bool m_show_auto_colorize = false; + MMUAutoColorizationParams m_auto_colorize_params; + + // Preview auto-colorization without applying it + void preview_auto_colorization(); + + // Apply auto-colorization to the model + void apply_auto_colorization(); + + // Render the auto-colorization UI section + void render_auto_colorization_ui(float x, float y, float bottom_limit, float window_width); }; } // namespace Slic3r From a03cfe3efc2cd925b1846d40308e21c7e70b8bf0 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 22 May 2025 03:07:36 -0400 Subject: [PATCH 2/4] Check pt working v0 --- .../MultiMaterialAutoColorization.cpp | 10 +++---- .../MultiMaterialAutoColorization.hpp | 2 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 27 +++++++++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/libslic3r/MultiMaterialAutoColorization.cpp b/src/libslic3r/MultiMaterialAutoColorization.cpp index 5f0039f8067..83cbef47022 100644 --- a/src/libslic3r/MultiMaterialAutoColorization.cpp +++ b/src/libslic3r/MultiMaterialAutoColorization.cpp @@ -484,7 +484,7 @@ void apply_auto_colorization(ModelObject& model_object, const MMUAutoColorizatio } // Generate a preview of the auto-colorization without modifying the model -std::vector> preview_auto_colorization( +std::vector> preview_auto_colorization( const ModelObject& model_object, const MMUAutoColorizationParams& params) { @@ -492,7 +492,7 @@ std::vector> preview_auto_colorization( MMUAutoColorizationParams validated_params = validate_auto_colorization_params(params); // Create a vector to store the triangle selectors - std::vector> selectors; + std::vector> result_selectors; // Process each volume in the model object for (const ModelVolume* volume : model_object.volumes) { @@ -500,7 +500,7 @@ std::vector> preview_auto_colorization( continue; // Create a triangle selector for this volume - auto selector = std::make_unique(volume->mesh()); + auto selector = std::make_unique(volume->mesh()); // Apply the selected pattern switch (validated_params.pattern_type) { @@ -526,10 +526,10 @@ std::vector> preview_auto_colorization( } // Add the selector to the vector - selectors.push_back(std::move(selector)); + result_selectors.push_back(std::move(selector)); } - return selectors; + return result_selectors; } } // namespace Slic3r diff --git a/src/libslic3r/MultiMaterialAutoColorization.hpp b/src/libslic3r/MultiMaterialAutoColorization.hpp index c251df4d29d..f7e0b6a2478 100644 --- a/src/libslic3r/MultiMaterialAutoColorization.hpp +++ b/src/libslic3r/MultiMaterialAutoColorization.hpp @@ -69,7 +69,7 @@ void apply_auto_colorization(ModelObject& model_object, const MMUAutoColorizatio // Generate a preview of the auto-colorization without modifying the model // Returns a vector of triangle selectors with the colorization applied -std::vector> preview_auto_colorization( +std::vector> preview_auto_colorization( const ModelObject& model_object, const MMUAutoColorizationParams& params); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index ae49e5f33c2..ac9d4d67d55 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -308,8 +308,13 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (!m_c->selection_info()->model_object()) return; - const float approx_height = m_imgui->scaled(25.35f); - y = std::min(y, bottom_limit - approx_height); + // Increase the approximate height to account for the auto-colorization section + const float approx_height = m_imgui->scaled(35.0f); + // Position the window higher up to ensure it fits on screen + y = std::min(y, bottom_limit - approx_height); + // Move the window up a bit to make room for the auto-colorization options + // but not too much to keep it visible + y -= m_imgui->scaled(20.0f); ImGuiPureWrap::set_next_window_pos(x, y, ImGuiCond_Always); ImGuiPureWrap::begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); @@ -578,7 +583,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::Separator(); // Add auto-colorization section - if (ImGuiPureWrap::collapsing_header(m_desc.at("auto_colorize"), ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader(m_desc.at("auto_colorize").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { render_auto_colorization_ui(x, y, bottom_limit, ImGui::GetContentRegionAvail().x); } @@ -890,7 +895,7 @@ void GLGizmoMmuSegmentation::preview_auto_colorization() UndoRedo::SnapshotType::GizmoAction); // Generate a preview of the auto-colorization - auto preview_selectors = preview_auto_colorization(*mo, m_auto_colorize_params); + auto preview_selectors = Slic3r::preview_auto_colorization(*mo, m_auto_colorize_params); // Apply the preview to the triangle selectors int idx = -1; @@ -922,7 +927,7 @@ void GLGizmoMmuSegmentation::apply_auto_colorization() UndoRedo::SnapshotType::GizmoAction); // Apply the auto-colorization to the model object - apply_auto_colorization(*mo, m_auto_colorize_params); + Slic3r::apply_auto_colorization(*mo, m_auto_colorize_params); // Update the triangle selectors from the model update_from_model_object(); @@ -965,7 +970,7 @@ void GLGizmoMmuSegmentation::render_auto_colorization_ui(float x, float y, float // Checkbox to enable/disable this extruder bool extruder_enabled = m_auto_colorize_params.extruders[i] > 0; - if (ImGuiPureWrap::checkbox(m_desc.at("extruder_use") + " " + std::to_string(i + 1), &extruder_enabled)) { + if (ImGui::Checkbox((m_desc.at("extruder_use") + " " + std::to_string(i + 1)).c_str(), &extruder_enabled)) { m_auto_colorize_params.extruders[i] = extruder_enabled ? int(i + 1) : 0; } @@ -1002,7 +1007,7 @@ void GLGizmoMmuSegmentation::render_auto_colorization_ui(float x, float y, float m_imgui->slider_float("##height_end", &m_auto_colorize_params.height_end_percent, 0.0f, 100.0f, "%.1f%%"); // Reverse direction - ImGuiPureWrap::checkbox(m_desc.at("height_reverse"), &m_auto_colorize_params.height_reverse); + ImGui::Checkbox(m_desc.at("height_reverse").c_str(), &m_auto_colorize_params.height_reverse); break; } @@ -1015,7 +1020,7 @@ void GLGizmoMmuSegmentation::render_auto_colorization_ui(float x, float y, float m_imgui->slider_float("##radial_radius", &m_auto_colorize_params.radial_radius, 1.0f, 200.0f, "%.1f"); // Reverse direction - ImGuiPureWrap::checkbox(m_desc.at("radial_reverse"), &m_auto_colorize_params.radial_reverse); + ImGui::Checkbox(m_desc.at("radial_reverse").c_str(), &m_auto_colorize_params.radial_reverse); break; } @@ -1038,7 +1043,7 @@ void GLGizmoMmuSegmentation::render_auto_colorization_ui(float x, float y, float } // Reverse direction - ImGuiPureWrap::checkbox(m_desc.at("spiral_reverse"), &m_auto_colorize_params.spiral_reverse); + ImGui::Checkbox(m_desc.at("spiral_reverse").c_str(), &m_auto_colorize_params.spiral_reverse); break; } @@ -1074,13 +1079,13 @@ void GLGizmoMmuSegmentation::render_auto_colorization_ui(float x, float y, float ImGui::Separator(); // Preview and Apply buttons - if (ImGuiPureWrap::button(m_desc.at("preview"), ImVec2(window_width * 0.48f, 0))) { + if (ImGui::Button(m_desc.at("preview").c_str(), ImVec2(window_width * 0.48f, 0))) { preview_auto_colorization(); } ImGui::SameLine(); - if (ImGuiPureWrap::button(m_desc.at("apply"), ImVec2(window_width * 0.48f, 0))) { + if (ImGui::Button(m_desc.at("apply").c_str(), ImVec2(window_width * 0.48f, 0))) { apply_auto_colorization(); } } From f9ef21c8d199d392839d114df48b9c7fa896ab80 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 22 May 2025 03:19:46 -0400 Subject: [PATCH 3/4] Correction --- src/libslic3r/MultiMaterialAutoColorization.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libslic3r/MultiMaterialAutoColorization.cpp b/src/libslic3r/MultiMaterialAutoColorization.cpp index 83cbef47022..97b86e908ad 100644 --- a/src/libslic3r/MultiMaterialAutoColorization.cpp +++ b/src/libslic3r/MultiMaterialAutoColorization.cpp @@ -197,8 +197,13 @@ void apply_radial_gradient(TriangleSelector& selector, const ModelVolume& volume const TriangleMesh& mesh = volume.mesh(); const Transform3d& volume_transform = volume.get_matrix(); + // Calculate the center of the mesh for determining outward-facing triangles + Vec3d mesh_center = mesh.bounding_box().center().cast(); + mesh_center = volume_transform * mesh_center; + // Process each triangle for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; // Calculate the center of the triangle @@ -239,8 +244,13 @@ void apply_spiral_pattern(TriangleSelector& selector, const ModelVolume& volume, const TriangleMesh& mesh = volume.mesh(); const Transform3d& volume_transform = volume.get_matrix(); + // Calculate the center of the mesh for determining outward-facing triangles + Vec3d mesh_center = mesh.bounding_box().center().cast(); + mesh_center = volume_transform * mesh_center; + // Process each triangle for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + const stl_triangle_vertex_indices& indices = mesh.its.indices[i]; // Calculate the center of the triangle From c61c65594a262fe63b54ab333b4a49cad676e479 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 22 May 2025 21:06:45 -0400 Subject: [PATCH 4/4] test --- tests/libslic3r/CMakeLists.txt | 5 +- .../libslic3r/test_mmu_auto_colorization.cpp | 228 ++++++++++++++++++ 2 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 tests/libslic3r/test_mmu_auto_colorization.cpp diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 8f11e14f87a..4669a55d8b3 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -35,7 +35,7 @@ add_executable(${_TEST_NAME}_tests test_png_io.cpp test_surface_mesh.cpp test_timeutils.cpp - test_quadric_edge_collapse.cpp + test_quadric_edge_collapse.cpp test_triangulation.cpp test_emboss.cpp test_indexed_triangle_set.cpp @@ -44,9 +44,10 @@ add_executable(${_TEST_NAME}_tests test_jump_point_search.cpp test_support_spots_generator.cpp test_layer_region.cpp + test_mmu_auto_colorization.cpp ../data/prusaparts.cpp ../data/prusaparts.hpp - test_static_map.cpp + test_static_map.cpp ) if (TARGET OpenVDB::openvdb) diff --git a/tests/libslic3r/test_mmu_auto_colorization.cpp b/tests/libslic3r/test_mmu_auto_colorization.cpp new file mode 100644 index 00000000000..769e1e9afed --- /dev/null +++ b/tests/libslic3r/test_mmu_auto_colorization.cpp @@ -0,0 +1,228 @@ +#include +#include "libslic3r/libslic3r.h" + +#include "libslic3r/MultiMaterialAutoColorization.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleSelector.hpp" + +using namespace Slic3r; + +// Helper function to create a simple cube model for testing +ModelObject create_test_cube(double size = 20.0) { + ModelObject obj; + ModelVolume* volume = obj.add_volume(TriangleMesh::make_cube(size, size, size)); + volume->set_type(ModelVolumeType::MODEL_PART); + return obj; +} + +SCENARIO("MMU Auto-Colorization Parameter Validation", "[MMUAutoColorization]") { + GIVEN("Default auto-colorization parameters") { + MMUAutoColorizationParams params; + + WHEN("Validating parameters") { + MMUAutoColorizationParams validated = validate_auto_colorization_params(params); + + THEN("Parameters are properly validated") { + // Check that at least one extruder is active + bool has_active_extruder = false; + for (int e : validated.extruders) { + if (e > 0) { + has_active_extruder = true; + break; + } + } + REQUIRE(has_active_extruder); + + // Check that distribution values are normalized + float total_distribution = 0.0f; + for (float d : validated.distribution) { + total_distribution += d; + } + REQUIRE(total_distribution == Approx(100.0f).epsilon(0.01f)); + } + } + } + + GIVEN("Invalid auto-colorization parameters") { + MMUAutoColorizationParams params; + // Set all extruders to inactive + for (size_t i = 0; i < params.extruders.size(); ++i) { + params.extruders[i] = 0; + } + + WHEN("Validating parameters") { + MMUAutoColorizationParams validated = validate_auto_colorization_params(params); + + THEN("At least one extruder is activated") { + bool has_active_extruder = false; + for (int e : validated.extruders) { + if (e > 0) { + has_active_extruder = true; + break; + } + } + REQUIRE(has_active_extruder); + } + } + } + + GIVEN("Parameters with invalid height range") { + MMUAutoColorizationParams params; + params.height_start_percent = -10.0f; + params.height_end_percent = 110.0f; + + WHEN("Validating parameters") { + MMUAutoColorizationParams validated = validate_auto_colorization_params(params); + + THEN("Height range is clamped to valid values") { + REQUIRE(validated.height_start_percent == 0.0f); + REQUIRE(validated.height_end_percent == 100.0f); + } + } + } + + GIVEN("Parameters with invalid radial and spiral values") { + MMUAutoColorizationParams params; + params.radial_radius = -5.0f; + params.spiral_pitch = 0.0f; + params.spiral_turns = 0; + + WHEN("Validating parameters") { + MMUAutoColorizationParams validated = validate_auto_colorization_params(params); + + THEN("Values are adjusted to valid minimums") { + REQUIRE(validated.radial_radius > 0.0f); + REQUIRE(validated.spiral_pitch > 0.0f); + REQUIRE(validated.spiral_turns >= 1); + } + } + } +} + +SCENARIO("MMU Auto-Colorization Color Assignment", "[MMUAutoColorization]") { + GIVEN("Distribution values and extruders") { + std::vector extruders = {1, 2, 3, 0, 0}; + std::vector distribution = {30.0f, 30.0f, 40.0f, 0.0f, 0.0f}; + + WHEN("Assigning colors based on normalized values") { + int color1 = assign_color_from_distribution(0.0f, extruders, distribution); + int color2 = assign_color_from_distribution(0.29f, extruders, distribution); + int color3 = assign_color_from_distribution(0.3f, extruders, distribution); + int color4 = assign_color_from_distribution(0.59f, extruders, distribution); + int color5 = assign_color_from_distribution(0.6f, extruders, distribution); + int color6 = assign_color_from_distribution(1.0f, extruders, distribution); + + THEN("Colors are assigned according to distribution") { + REQUIRE(color1 == 1); + REQUIRE(color2 == 1); + REQUIRE(color3 == 2); + REQUIRE(color4 == 2); + REQUIRE(color5 == 3); + REQUIRE(color6 == 3); + } + } + } + + GIVEN("Empty extruders or distribution") { + std::vector empty_extruders; + std::vector empty_distribution; + std::vector valid_extruders = {1, 2, 3}; + std::vector valid_distribution = {30.0f, 30.0f, 40.0f}; + + WHEN("Assigning colors with empty data") { + int color1 = assign_color_from_distribution(0.5f, empty_extruders, valid_distribution); + int color2 = assign_color_from_distribution(0.5f, valid_extruders, empty_distribution); + + THEN("Default color (0) is returned") { + REQUIRE(color1 == 0); + REQUIRE(color2 == 0); + } + } + } +} + +SCENARIO("MMU Auto-Colorization Pattern Application", "[MMUAutoColorization]") { + GIVEN("A simple cube model") { + ModelObject obj = create_test_cube(); + MMUAutoColorizationParams params; + params.extruders = {1, 2, 0, 0, 0}; + params.distribution = {50.0f, 50.0f, 0.0f, 0.0f, 0.0f}; + + WHEN("Applying height gradient pattern") { + params.pattern_type = MMUAutoColorizationPattern::HeightGradient; + auto selectors = preview_auto_colorization(obj, params); + + THEN("Selectors are created for each volume") { + REQUIRE(selectors.size() == obj.volumes.size()); + + // Check that some triangles are colored + bool has_colored_triangles = false; + for (const auto& selector : selectors) { + if (selector->get_triangle_count() > 0) { + has_colored_triangles = true; + break; + } + } + REQUIRE(has_colored_triangles); + } + } + + WHEN("Applying radial gradient pattern") { + params.pattern_type = MMUAutoColorizationPattern::RadialGradient; + auto selectors = preview_auto_colorization(obj, params); + + THEN("Selectors are created for each volume") { + REQUIRE(selectors.size() == obj.volumes.size()); + } + } + + WHEN("Applying spiral pattern") { + params.pattern_type = MMUAutoColorizationPattern::SpiralPattern; + auto selectors = preview_auto_colorization(obj, params); + + THEN("Selectors are created for each volume") { + REQUIRE(selectors.size() == obj.volumes.size()); + } + } + + WHEN("Applying noise pattern") { + params.pattern_type = MMUAutoColorizationPattern::NoisePattern; + auto selectors = preview_auto_colorization(obj, params); + + THEN("Selectors are created for each volume") { + REQUIRE(selectors.size() == obj.volumes.size()); + } + } + + WHEN("Applying optimized changes pattern") { + params.pattern_type = MMUAutoColorizationPattern::OptimizedChanges; + auto selectors = preview_auto_colorization(obj, params); + + THEN("Selectors are created for each volume") { + REQUIRE(selectors.size() == obj.volumes.size()); + } + } + } +} + +SCENARIO("MMU Auto-Colorization Direct Application", "[MMUAutoColorization]") { + GIVEN("A simple cube model") { + ModelObject obj = create_test_cube(); + MMUAutoColorizationParams params; + params.extruders = {1, 2, 0, 0, 0}; + params.distribution = {50.0f, 50.0f, 0.0f, 0.0f, 0.0f}; + + WHEN("Applying auto-colorization directly") { + apply_auto_colorization(obj, params); + + THEN("Model volumes have segmentation data") { + for (const ModelVolume* volume : obj.volumes) { + if (volume->is_model_part()) { + // Check that segmentation data is not empty + REQUIRE(volume->mm_segmentation_facets.get_data().size() > 0); + } + } + } + } + } +}