diff --git a/CMakeLists.txt b/CMakeLists.txt index 64d042db4e..c278a25878 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -427,8 +427,8 @@ if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMP if((${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang") AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 15) include(CheckCXXCompilerFlag) - check_cxx_compiler_flag(-Wno-error=enum-constexpr-conversion HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV) - if(HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV) + check_cxx_compiler_flag("-Werror=unknown-warning-option -Wno-error=enum-constexpr-conversion" HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV_STRICT) + if(HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV_STRICT) add_compile_options(-Wno-error=enum-constexpr-conversion) endif() endif() diff --git a/resources/shaders/110/painted_texture_preview.fs b/resources/shaders/110/painted_texture_preview.fs new file mode 100644 index 0000000000..d41fbf5d51 --- /dev/null +++ b/resources/shaders/110/painted_texture_preview.fs @@ -0,0 +1,66 @@ +#version 110 + +const vec3 ZERO = vec3(0.0, 0.0, 0.0); +const float UV_EDGE_EPSILON = 0.000001; + +struct PrintVolumeDetection +{ + int type; + vec4 xy_data; + vec2 z_data; +}; + +uniform vec4 uniform_color; +uniform sampler2D uniform_texture; +uniform float texture_preview_mix; +uniform bool invalid_texture_mapping; +uniform PrintVolumeDetection print_volume; + +varying vec2 intensity; +varying vec3 clipping_planes_dots; +varying vec4 world_pos; +varying vec2 tex_coord; + +float texture_preview_coord(float uv) +{ + if (uv >= -UV_EDGE_EPSILON && uv <= 1.0 + UV_EDGE_EPSILON) + return clamp(uv, 0.0, 1.0); + + return fract(uv); +} + +vec2 texture_preview_coord(vec2 uv) +{ + return vec2(texture_preview_coord(uv.x), texture_preview_coord(uv.y)); +} + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + vec4 color = uniform_color; + vec4 texture_color = texture2D(uniform_texture, texture_preview_coord(tex_coord)); + float mix_factor = clamp(texture_preview_mix, 0.0, 1.0); + color.rgb = mix(color.rgb, texture_color.rgb, mix_factor); + if (invalid_texture_mapping) { + float checker = mod(floor(tex_coord.x * 24.0) + floor(tex_coord.y * 24.0), 2.0); + vec3 checker_color = mix(vec3(0.0), vec3(1.0), checker); + color.rgb = mix(color.rgb, checker_color, 0.62); + } + + vec3 pv_check_min = ZERO; + vec3 pv_check_max = ZERO; + if (print_volume.type == 0) { + pv_check_min = world_pos.xyz - vec3(print_volume.xy_data.x, print_volume.xy_data.y, print_volume.z_data.x); + pv_check_max = world_pos.xyz - vec3(print_volume.xy_data.z, print_volume.xy_data.w, print_volume.z_data.y); + } + else if (print_volume.type == 1) { + float delta_radius = print_volume.xy_data.z - distance(world_pos.xy, print_volume.xy_data.xy); + pv_check_min = vec3(delta_radius, 0.0, world_pos.z - print_volume.z_data.x); + pv_check_max = vec3(0.0, 0.0, world_pos.z - print_volume.z_data.y); + } + + color.rgb = (any(lessThan(pv_check_min, ZERO)) || any(greaterThan(pv_check_max, ZERO))) ? mix(color.rgb, ZERO, 0.3333) : color.rgb; + gl_FragColor = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a); +} diff --git a/resources/shaders/110/painted_texture_preview.vs b/resources/shaders/110/painted_texture_preview.vs new file mode 100644 index 0000000000..b56566dac6 --- /dev/null +++ b/resources/shaders/110/painted_texture_preview.vs @@ -0,0 +1,47 @@ +#version 110 + +#define INTENSITY_CORRECTION 0.6 + +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; +attribute vec2 v_tex_coord; + +varying vec2 intensity; +varying vec3 clipping_planes_dots; +varying vec4 world_pos; +varying vec2 tex_coord; + +void main() +{ + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + world_pos = volume_world_matrix * vec4(v_position, 1.0); + tex_coord = v_tex_coord; + gl_Position = projection_matrix * position; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); +} diff --git a/resources/shaders/110/painted_vertex_color_preview.fs b/resources/shaders/110/painted_vertex_color_preview.fs new file mode 100644 index 0000000000..399f0b8b7d --- /dev/null +++ b/resources/shaders/110/painted_vertex_color_preview.fs @@ -0,0 +1,50 @@ +#version 110 + +const vec3 ZERO = vec3(0.0, 0.0, 0.0); + +struct PrintVolumeDetection +{ + int type; + vec4 xy_data; + vec2 z_data; +}; + +uniform vec4 uniform_color; +uniform float texture_preview_mix; +uniform bool invalid_texture_mapping; +uniform PrintVolumeDetection print_volume; + +varying vec2 intensity; +varying vec3 clipping_planes_dots; +varying vec4 world_pos; +varying vec4 vertex_color; + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + vec4 color = uniform_color; + float mix_factor = clamp(texture_preview_mix, 0.0, 1.0); + color.rgb = mix(color.rgb, vertex_color.rgb, mix_factor); + if (invalid_texture_mapping) { + float checker = mod(floor(world_pos.x * 4.0) + floor(world_pos.y * 4.0) + floor(world_pos.z * 4.0), 2.0); + vec3 checker_color = mix(vec3(0.0), vec3(1.0), checker); + color.rgb = mix(color.rgb, checker_color, 0.62); + } + + vec3 pv_check_min = ZERO; + vec3 pv_check_max = ZERO; + if (print_volume.type == 0) { + pv_check_min = world_pos.xyz - vec3(print_volume.xy_data.x, print_volume.xy_data.y, print_volume.z_data.x); + pv_check_max = world_pos.xyz - vec3(print_volume.xy_data.z, print_volume.xy_data.w, print_volume.z_data.y); + } + else if (print_volume.type == 1) { + float delta_radius = print_volume.xy_data.z - distance(world_pos.xy, print_volume.xy_data.xy); + pv_check_min = vec3(delta_radius, 0.0, world_pos.z - print_volume.z_data.x); + pv_check_max = vec3(0.0, 0.0, world_pos.z - print_volume.z_data.y); + } + + color.rgb = (any(lessThan(pv_check_min, ZERO)) || any(greaterThan(pv_check_max, ZERO))) ? mix(color.rgb, ZERO, 0.3333) : color.rgb; + gl_FragColor = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a); +} diff --git a/resources/shaders/110/painted_vertex_color_preview.vs b/resources/shaders/110/painted_vertex_color_preview.vs new file mode 100644 index 0000000000..822daad707 --- /dev/null +++ b/resources/shaders/110/painted_vertex_color_preview.vs @@ -0,0 +1,47 @@ +#version 110 + +#define INTENSITY_CORRECTION 0.6 + +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; +attribute vec4 v_color; + +varying vec2 intensity; +varying vec3 clipping_planes_dots; +varying vec4 world_pos; +varying vec4 vertex_color; + +void main() +{ + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + world_pos = volume_world_matrix * vec4(v_position, 1.0); + vertex_color = v_color; + gl_Position = projection_matrix * position; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); +} diff --git a/resources/shaders/140/painted_texture_preview.fs b/resources/shaders/140/painted_texture_preview.fs new file mode 100644 index 0000000000..f19ca32ca7 --- /dev/null +++ b/resources/shaders/140/painted_texture_preview.fs @@ -0,0 +1,68 @@ +#version 140 + +const vec3 ZERO = vec3(0.0, 0.0, 0.0); +const float UV_EDGE_EPSILON = 0.000001; + +struct PrintVolumeDetection +{ + int type; + vec4 xy_data; + vec2 z_data; +}; + +uniform vec4 uniform_color; +uniform sampler2D uniform_texture; +uniform float texture_preview_mix; +uniform bool invalid_texture_mapping; +uniform PrintVolumeDetection print_volume; + +in vec2 intensity; +in vec3 clipping_planes_dots; +in vec4 world_pos; +in vec2 tex_coord; + +out vec4 out_color; + +float texture_preview_coord(float uv) +{ + if (uv >= -UV_EDGE_EPSILON && uv <= 1.0 + UV_EDGE_EPSILON) + return clamp(uv, 0.0, 1.0); + + return fract(uv); +} + +vec2 texture_preview_coord(vec2 uv) +{ + return vec2(texture_preview_coord(uv.x), texture_preview_coord(uv.y)); +} + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + vec4 color = uniform_color; + vec4 texture_color = texture(uniform_texture, texture_preview_coord(tex_coord)); + float mix_factor = clamp(texture_preview_mix, 0.0, 1.0); + color.rgb = mix(color.rgb, texture_color.rgb, mix_factor); + if (invalid_texture_mapping) { + float checker = mod(floor(tex_coord.x * 24.0) + floor(tex_coord.y * 24.0), 2.0); + vec3 checker_color = mix(vec3(0.0), vec3(1.0), checker); + color.rgb = mix(color.rgb, checker_color, 0.62); + } + + vec3 pv_check_min = ZERO; + vec3 pv_check_max = ZERO; + if (print_volume.type == 0) { + pv_check_min = world_pos.xyz - vec3(print_volume.xy_data.x, print_volume.xy_data.y, print_volume.z_data.x); + pv_check_max = world_pos.xyz - vec3(print_volume.xy_data.z, print_volume.xy_data.w, print_volume.z_data.y); + } + else if (print_volume.type == 1) { + float delta_radius = print_volume.xy_data.z - distance(world_pos.xy, print_volume.xy_data.xy); + pv_check_min = vec3(delta_radius, 0.0, world_pos.z - print_volume.z_data.x); + pv_check_max = vec3(0.0, 0.0, world_pos.z - print_volume.z_data.y); + } + + color.rgb = (any(lessThan(pv_check_min, ZERO)) || any(greaterThan(pv_check_max, ZERO))) ? mix(color.rgb, ZERO, 0.3333) : color.rgb; + out_color = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a); +} diff --git a/resources/shaders/140/painted_texture_preview.vs b/resources/shaders/140/painted_texture_preview.vs new file mode 100644 index 0000000000..894ea84e89 --- /dev/null +++ b/resources/shaders/140/painted_texture_preview.vs @@ -0,0 +1,47 @@ +#version 140 + +#define INTENSITY_CORRECTION 0.6 + +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +in vec3 v_position; +in vec3 v_normal; +in vec2 v_tex_coord; + +out vec2 intensity; +out vec3 clipping_planes_dots; +out vec4 world_pos; +out vec2 tex_coord; + +void main() +{ + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + world_pos = volume_world_matrix * vec4(v_position, 1.0); + tex_coord = v_tex_coord; + gl_Position = projection_matrix * position; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); +} diff --git a/resources/shaders/140/painted_vertex_color_preview.fs b/resources/shaders/140/painted_vertex_color_preview.fs new file mode 100644 index 0000000000..b4f5684a4d --- /dev/null +++ b/resources/shaders/140/painted_vertex_color_preview.fs @@ -0,0 +1,52 @@ +#version 140 + +const vec3 ZERO = vec3(0.0, 0.0, 0.0); + +struct PrintVolumeDetection +{ + int type; + vec4 xy_data; + vec2 z_data; +}; + +uniform vec4 uniform_color; +uniform float texture_preview_mix; +uniform bool invalid_texture_mapping; +uniform PrintVolumeDetection print_volume; + +in vec2 intensity; +in vec3 clipping_planes_dots; +in vec4 world_pos; +in vec4 vertex_color; + +out vec4 out_color; + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + vec4 color = uniform_color; + float mix_factor = clamp(texture_preview_mix, 0.0, 1.0); + color.rgb = mix(color.rgb, vertex_color.rgb, mix_factor); + if (invalid_texture_mapping) { + float checker = mod(floor(world_pos.x * 4.0) + floor(world_pos.y * 4.0) + floor(world_pos.z * 4.0), 2.0); + vec3 checker_color = mix(vec3(0.0), vec3(1.0), checker); + color.rgb = mix(color.rgb, checker_color, 0.62); + } + + vec3 pv_check_min = ZERO; + vec3 pv_check_max = ZERO; + if (print_volume.type == 0) { + pv_check_min = world_pos.xyz - vec3(print_volume.xy_data.x, print_volume.xy_data.y, print_volume.z_data.x); + pv_check_max = world_pos.xyz - vec3(print_volume.xy_data.z, print_volume.xy_data.w, print_volume.z_data.y); + } + else if (print_volume.type == 1) { + float delta_radius = print_volume.xy_data.z - distance(world_pos.xy, print_volume.xy_data.xy); + pv_check_min = vec3(delta_radius, 0.0, world_pos.z - print_volume.z_data.x); + pv_check_max = vec3(0.0, 0.0, world_pos.z - print_volume.z_data.y); + } + + color.rgb = (any(lessThan(pv_check_min, ZERO)) || any(greaterThan(pv_check_max, ZERO))) ? mix(color.rgb, ZERO, 0.3333) : color.rgb; + out_color = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a); +} diff --git a/resources/shaders/140/painted_vertex_color_preview.vs b/resources/shaders/140/painted_vertex_color_preview.vs new file mode 100644 index 0000000000..2fb384e8fd --- /dev/null +++ b/resources/shaders/140/painted_vertex_color_preview.vs @@ -0,0 +1,47 @@ +#version 140 + +#define INTENSITY_CORRECTION 0.6 + +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +in vec3 v_position; +in vec3 v_normal; +in vec4 v_color; + +out vec2 intensity; +out vec3 clipping_planes_dots; +out vec4 world_pos; +out vec4 vertex_color; + +void main() +{ + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + world_pos = volume_world_matrix * vec4(v_position, 1.0); + vertex_color = v_color; + gl_Position = projection_matrix * position; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); +} diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 5f9591452f..1f35c414e0 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -137,6 +137,9 @@ set(lisbslic3r_sources Feature/Interlocking/VoxelUtils.cpp Feature/Interlocking/VoxelUtils.hpp FileParserError.hpp + filament_mixer.cpp + filament_mixer.h + filament_mixer_model.h Fill/Fill3DHoneycomb.cpp Fill/Fill3DHoneycomb.hpp Fill/FillAdaptive.cpp @@ -457,6 +460,8 @@ set(lisbslic3r_sources TriangleSelector.hpp TriangleSetSampling.cpp TriangleSetSampling.hpp + TextureMapping.cpp + TextureMapping.hpp TriangulateWall.cpp TriangulateWall.hpp utils.cpp diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index 71f7d1e7e2..e2e77af50d 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -6,7 +6,9 @@ #include "objparser.hpp" #include +#include +#include #include #ifdef _WIN32 @@ -34,37 +36,47 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s return false; } bool exist_mtl = false; + boost::filesystem::path obj_path(path); + const boost::filesystem::path obj_dir = obj_path.parent_path(); + obj_info.obj_dircetory = obj_dir.string(); + std::vector mtl_warnings; if (data.mtllibs.size() > 0) { // read mtl for (auto mtl_name : data.mtllibs) { if (mtl_name.size() == 0){ continue; } exist_mtl = true; - bool mtl_name_is_path = false; - boost::filesystem::path mtl_abs_path(mtl_name); - if (boost::filesystem::exists(mtl_abs_path)) { - mtl_name_is_path = true; + + const boost::filesystem::path raw_mtl_path(mtl_name); + boost::filesystem::path resolved_mtl_path; + if (raw_mtl_path.is_absolute()) { + if (boost::filesystem::exists(raw_mtl_path)) + resolved_mtl_path = raw_mtl_path; + else + resolved_mtl_path = obj_dir / raw_mtl_path.filename(); + } else { + const boost::filesystem::path relative_path = obj_dir / raw_mtl_path; + if (boost::filesystem::exists(relative_path)) + resolved_mtl_path = relative_path; + else + resolved_mtl_path = obj_dir / raw_mtl_path.filename(); } - boost::filesystem::path mtl_path; - if (!mtl_name_is_path) { - boost::filesystem::path full_path(path); - std::string dir = full_path.parent_path().string(); - auto mtl_file = dir + "/" + mtl_name; - boost::filesystem::path temp_mtl_path(mtl_file); - mtl_path = temp_mtl_path; - } - auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str(); - if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) { - if (!ObjParser::mtlparse(_mtl_path, mtl_data)) { - BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path; - message = _L("load mtl in obj: failed to parse"); - return false; + + const std::string resolved_mtl_path_str = resolved_mtl_path.string(); + if (boost::filesystem::exists(resolved_mtl_path)) { + if (!ObjParser::mtlparse(resolved_mtl_path_str.c_str(), mtl_data)) { + BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << resolved_mtl_path_str; + mtl_warnings.emplace_back(resolved_mtl_path_str); } } else { - BOOST_LOG_TRIVIAL(error) << "load_obj: failed to load mtl_path:" << _mtl_path; + BOOST_LOG_TRIVIAL(error) << "load_obj: failed to load mtl_path:" << resolved_mtl_path_str; + mtl_warnings.emplace_back(resolved_mtl_path_str); } } + + if (!mtl_warnings.empty()) + message = _L("load mtl in obj: failed to parse or load; importing model without some material/texture data"); } // Count the faces and verify, that all faces are triangular. size_t num_faces = 0; @@ -96,6 +108,8 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s size_t num_vertices = data.coordinates.size() / OBJ_VERTEX_LENGTH; its.vertices.reserve(num_vertices); its.indices.reserve(num_faces + num_quads); + obj_info.triangle_uvs.reserve(num_faces + num_quads); + obj_info.triangle_uvs_valid.reserve(num_faces + num_quads); if (exist_mtl) { obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1; obj_info.face_colors.reserve(num_faces + num_quads); @@ -112,6 +126,24 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s } int indices[ONE_FACE_SIZE]; int uvs[ONE_FACE_SIZE]; + auto read_uv = [&data](int uv_idx, Vec2f &out_uv) { + if (uv_idx < 0) + return false; + const size_t off = size_t(uv_idx) * 2; + if (off + 1 >= data.textureCoordinates.size()) + return false; + out_uv = Vec2f(data.textureCoordinates[off], data.textureCoordinates[off + 1]); + return true; + }; + auto append_triangle_uv = [&obj_info, &read_uv](int uv0_idx, int uv1_idx, int uv2_idx) { + std::array triangle_uv{Vec2f::Zero(), Vec2f::Zero(), Vec2f::Zero()}; + const bool has_all_uv = + read_uv(uv0_idx, triangle_uv[0]) && + read_uv(uv1_idx, triangle_uv[1]) && + read_uv(uv2_idx, triangle_uv[2]); + obj_info.triangle_uvs.emplace_back(triangle_uv); + obj_info.triangle_uvs_valid.emplace_back(uint8_t(has_all_uv ? 1 : 0)); + }; for (size_t i = 0; i < data.vertices.size();) if (data.vertices[i].coordIdx == -1) ++ i; @@ -121,7 +153,12 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s if (const ObjParser::ObjVertex &vertex = data.vertices[i ++]; vertex.coordIdx == -1) { break; } else { - assert(cnt < OBJ_VERTEX_LENGTH); + assert(cnt < ONE_FACE_SIZE); + if (cnt >= ONE_FACE_SIZE) { + BOOST_LOG_TRIVIAL(error) << "load_obj: failed to parse " << path << ". The file contains polygons with more than 4 vertices."; + message = _L("The file contains polygons with more than 4 vertices."); + return false; + } if (vertex.coordIdx < 0 || vertex.coordIdx >= int(its.vertices.size())) { BOOST_LOG_TRIVIAL(error) << "load_obj: failed to parse " << path << ". The file contains invalid vertex index."; message = _L("The file contains invalid vertex index."); @@ -135,38 +172,46 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s assert(cnt == 3 || cnt == 4); // Insert one or two faces (triangulate a quad). its.indices.emplace_back(indices[0], indices[1], indices[2]); + append_triangle_uv(uvs[0], uvs[1], uvs[2]); int face_index =its.indices.size() - 1; - RGBA face_color; - auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name) { - if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) { + auto set_face_color = [&data, &mtl_data, &obj_info, &read_uv] + (int face_index, const std::string &mtl_name, const std::array &triangle_uv_indices) { + const auto material_it = mtl_data.new_mtl_unmap.find(mtl_name); + if (material_it != mtl_data.new_mtl_unmap.end() && material_it->second) { + const auto &material = *material_it->second; + RGBA face_color; bool is_merge_ka_kd = true; for (size_t n = 0; n < 3; n++) { - if (float(mtl_data.new_mtl_unmap[mtl_name]->Ka[n] + mtl_data.new_mtl_unmap[mtl_name]->Kd[n]) > 1.0) { + if (float(material.Ka[n] + material.Kd[n]) > 1.0) { is_merge_ka_kd=false; break; } } for (size_t n = 0; n < 3; n++) { if (is_merge_ka_kd) { - face_color[n] = std::clamp(float(mtl_data.new_mtl_unmap[mtl_name]->Ka[n] + mtl_data.new_mtl_unmap[mtl_name]->Kd[n]), 0.f, 1.f); + face_color[n] = std::clamp(float(material.Ka[n] + material.Kd[n]), 0.f, 1.f); } else { - face_color[n] = std::clamp(float(mtl_data.new_mtl_unmap[mtl_name]->Kd[n]), 0.f, 1.f); + face_color[n] = std::clamp(float(material.Kd[n]), 0.f, 1.f); } } - face_color[3] = mtl_data.new_mtl_unmap[mtl_name]->Tr; // alpha - if (mtl_data.new_mtl_unmap[mtl_name]->map_Kd.size() > 0) { - auto png_name = mtl_data.new_mtl_unmap[mtl_name]->map_Kd; + face_color[3] = material.Tr; // alpha + if (!material.map_Kd.empty()) { + const std::string &png_name = material.map_Kd; obj_info.has_uv_png = true; - if (obj_info.pngs.find(png_name) == obj_info.pngs.end()) { obj_info.pngs[png_name] = false; } + obj_info.pngs.emplace(png_name, false); obj_info.uv_map_pngs[face_index] = png_name; } if (data.textureCoordinates.size() > 0) { - Vec2f uv0(data.textureCoordinates[uvs[0] * 2], data.textureCoordinates[uvs[0] * 2 + 1]); - Vec2f uv1(data.textureCoordinates[uvs[1] * 2], data.textureCoordinates[uvs[1] * 2 + 1]); - Vec2f uv2(data.textureCoordinates[uvs[2] * 2], data.textureCoordinates[uvs[2] * 2 + 1]); - std::array uv_array{uv0, uv1, uv2}; - obj_info.uvs.emplace_back(uv_array); + Vec2f uv0 = Vec2f::Zero(); + Vec2f uv1 = Vec2f::Zero(); + Vec2f uv2 = Vec2f::Zero(); + if (read_uv(triangle_uv_indices[0], uv0) && + read_uv(triangle_uv_indices[1], uv1) && + read_uv(triangle_uv_indices[2], uv2)) { + std::array uv_array{uv0, uv1, uv2}; + obj_info.uvs.emplace_back(uv_array); + } } obj_info.face_colors.emplace_back(face_color); } @@ -176,40 +221,53 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s } } }; - auto set_face_color_by_mtl = [&data, &set_face_color](int face_index) { + auto set_face_color_by_mtl = [&data, &set_face_color](int face_index, const std::array &triangle_uv_indices) { if (data.usemtls.size() == 1) { - set_face_color(face_index, data.usemtls[0].name); + set_face_color(face_index, data.usemtls[0].name, triangle_uv_indices); } else { for (size_t k = 0; k < data.usemtls.size(); k++) { auto mtl = data.usemtls[k]; if (face_index >= mtl.face_start && face_index <= mtl.face_end) { - set_face_color(face_index, data.usemtls[k].name); + set_face_color(face_index, data.usemtls[k].name, triangle_uv_indices); break; } } } }; if (exist_mtl) { - set_face_color_by_mtl(face_index); + set_face_color_by_mtl(face_index, {uvs[0], uvs[1], uvs[2]}); } if (cnt == 4) { its.indices.emplace_back(indices[0], indices[2], indices[3]); + append_triangle_uv(uvs[0], uvs[2], uvs[3]); int face_index = its.indices.size() - 1; if (exist_mtl) { - set_face_color_by_mtl(face_index); + set_face_color_by_mtl(face_index, {uvs[0], uvs[2], uvs[3]}); } } } } + if (obj_info.has_uv_png && !obj_info.uv_map_pngs.empty()) { + std::set unique_textures; + for (const auto &face_to_png : obj_info.uv_map_pngs) + if (!face_to_png.second.empty()) + unique_textures.insert(face_to_png.second); + if (unique_textures.size() == 1) + obj_info.single_texture_image = *unique_textures.begin(); + } + *meshptr = TriangleMesh(std::move(its)); if (meshptr->empty()) { BOOST_LOG_TRIVIAL(error) << "load_obj: This OBJ file couldn't be read because it's empty. " << path; message = _L("This OBJ file couldn't be read because it's empty."); return false; } - if (meshptr->volume() < 0) + if (meshptr->volume() < 0) { meshptr->flip_triangles(); + for (std::array &triangle_uv : obj_info.triangle_uvs) + std::swap(triangle_uv[1], triangle_uv[2]); + } return true; } diff --git a/src/libslic3r/Format/OBJ.hpp b/src/libslic3r/Format/OBJ.hpp index 2d4370c99a..0860ea517b 100644 --- a/src/libslic3r/Format/OBJ.hpp +++ b/src/libslic3r/Format/OBJ.hpp @@ -1,12 +1,37 @@ #ifndef slic3r_Format_OBJ_hpp_ #define slic3r_Format_OBJ_hpp_ #include "libslic3r/Color.hpp" +#include +#include +#include +#include +#include +#include #include +#include namespace Slic3r { class TriangleMesh; class Model; class ModelObject; + +enum class ObjImportMode { + UseDefault = 0, + ImportPaintedRegions, + ImportTextures, + ImportNeither +}; + +struct ObjImportCapabilities { + bool has_vertex_colors{false}; + bool has_face_colors{false}; + bool is_single_color{false}; + size_t texture_count{0}; + bool has_valid_texture_uvs{false}; +}; + +typedef std::function ObjImportModeFn; + // Load an OBJ file into a provided model. struct ObjInfo { std::vector vertex_colors; @@ -14,10 +39,13 @@ struct ObjInfo { bool is_single_mtl{false}; std::string lost_material_name{""}; std::vector> uvs; + std::vector> triangle_uvs; + std::vector triangle_uvs_valid; std::string obj_dircetory; std::map pngs; std::unordered_map uv_map_pngs; bool has_uv_png{false}; + std::string single_texture_image; }; struct ObjDialogInOut diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index e91d92309c..e80463e745 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -7,7 +7,9 @@ #include "../GCode.hpp" #include "../Geometry.hpp" #include "../GCode/ThumbnailData.hpp" +#include "../PNGReadWrite.hpp" #include "../Semver.hpp" +#include "../TextureMapping.hpp" #include "../Time.hpp" #include "../I18N.hpp" @@ -17,6 +19,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -56,6 +62,7 @@ namespace pt = boost::property_tree; #include "NSVGUtils.hpp" #include +#include // Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, // https://github.com/boostorg/spirit/pull/586 @@ -139,6 +146,27 @@ static bool is_path_within_root(const std::string& file_path, const boost::files return true; } +static bool is_texture_mapping_virtual_filament_id(const Slic3r::DynamicPrintConfig &config, int filament_id, size_t physical_count) +{ + if (filament_id < 99 || filament_id > 255) + return false; + + const auto *texture_defs_opt = config.option("texture_mapping_definitions"); + if (texture_defs_opt == nullptr || texture_defs_opt->value.empty()) + return false; + + std::vector physical_colors; + if (const auto *colors_opt = config.option("filament_colour"); + colors_opt != nullptr && !colors_opt->values.empty()) + physical_colors = colors_opt->values; + else + physical_colors.assign(physical_count, "#FFFFFF"); + + Slic3r::TextureMappingManager texture_mgr; + texture_mgr.load_entries(texture_defs_opt->value, physical_colors); + return texture_mgr.is_texture_mapping_zone_id(unsigned(filament_id)); +} + // VERSION NUMBERS // 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. // 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files. @@ -231,6 +259,11 @@ static constexpr const char* MODEL_TAG = "model"; static constexpr const char* RESOURCES_TAG = "resources"; static constexpr const char* COLOR_GROUP_TAG = "m:colorgroup"; static constexpr const char* COLOR_TAG = "m:color"; +static constexpr const char* TEXTURE_2D_TAG = "m:texture2d"; +static constexpr const char* TEXTURE_2D_GROUP_TAG = "m:texture2dgroup"; +static constexpr const char* TEX2COORD_TAG = "m:tex2coord"; +static constexpr const char* MULTI_PROPERTIES_TAG = "m:multiproperties"; +static constexpr const char* MULTI_TAG = "m:multi"; static constexpr const char* OBJECT_TAG = "object"; static constexpr const char* MESH_TAG = "mesh"; static constexpr const char* MESH_STAT_TAG = "mesh_stat"; @@ -308,6 +341,8 @@ static constexpr const char* ID_ATTR = "id"; static constexpr const char* X_ATTR = "x"; static constexpr const char* Y_ATTR = "y"; static constexpr const char* Z_ATTR = "z"; +static constexpr const char* U_ATTR = "u"; +static constexpr const char* V_ATTR = "v"; static constexpr const char* V1_ATTR = "v1"; static constexpr const char* V2_ATTR = "v2"; static constexpr const char* V3_ATTR = "v3"; @@ -327,8 +362,16 @@ static constexpr const char* FACE_PROPERTY_ATTR = "face_property"; static constexpr const char* KEY_ATTR = "key"; static constexpr const char* VALUE_ATTR = "value"; +static constexpr const char* TEXID_ATTR = "texid"; +static constexpr const char* PATH_ATTR = "path"; +static constexpr const char* CONTENTTYPE_ATTR = "contenttype"; +static constexpr const char* PIDS_ATTR = "pids"; +static constexpr const char* PINDICES_ATTR = "pindices"; static constexpr const char* FIRST_TRIANGLE_ID_ATTR = "firstid"; static constexpr const char* LAST_TRIANGLE_ID_ATTR = "lastid"; +static constexpr const char* P1_ATTR = "p1"; +static constexpr const char* P2_ATTR = "p2"; +static constexpr const char* P3_ATTR = "p3"; static constexpr const char* SUBTYPE_ATTR = "subtype"; static constexpr const char* LOCK_ATTR = "locked"; static constexpr const char* BED_TYPE_ATTR = "bed_type"; @@ -384,6 +427,10 @@ static constexpr const char* SOURCE_OFFSET_Z_KEY = "source_offset_z"; static constexpr const char* SOURCE_IN_INCHES = "source_in_inches"; static constexpr const char* SOURCE_IN_METERS = "source_in_meters"; +static constexpr const char *MATERIALS_NAMESPACE = "http://schemas.microsoft.com/3dmanufacturing/material/2015/02"; +static constexpr const char *MODEL_TEXTURE_REL_TYPE = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture"; +static constexpr const char *MODEL_TEXTURE_CONTENT_TYPE = "application/vnd.ms-package.3dmanufacturing-3dmodeltexture"; + static constexpr const char* MESH_SHARED_KEY = "mesh_shared"; static constexpr const char* MESH_STAT_EDGES_FIXED = "edges_fixed"; @@ -673,6 +720,511 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) #define L(s) (s) #define _(s) Slic3r::I18N::translate(s) +struct ThreeMfTexture2DResource +{ + std::string path; + std::string content_type; +}; + +struct ThreeMfTexture2DGroupResource +{ + int tex_id{-1}; + std::vector> coords; +}; + +struct ThreeMfColorGroupResource +{ + std::vector colors; +}; + +struct ThreeMfMultiPropertiesResource +{ + std::vector pids; + std::vector> pindices; +}; + +struct PendingThreeMfImportedTexture +{ + std::string image_file; + std::string image_content_type; + std::vector uvs_per_face; + std::vector uv_valid; +}; + +struct ThreeMfExportTextureResource +{ + int color_group_id{-1}; + int texture_id{-1}; + int texture_group_id{-1}; + int multi_properties_id{-1}; + std::string texture_part_path; + std::vector triangle_texcoord_starts; +}; + +using VolumeToThreeMfExportTextureMap = std::map; + +static bool has_imported_vertex_color_payload(const ModelVolume &volume) +{ + const size_t vertex_count = volume.mesh().its.vertices.size(); + return vertex_count > 0 && volume.imported_vertex_colors_rgba.size() == vertex_count; +} + +static bool has_imported_obj_texture_payload(const ModelVolume &volume) +{ + const size_t triangle_count = volume.mesh().its.indices.size(); + return volume.imported_texture_width > 0 && + volume.imported_texture_height > 0 && + !volume.imported_texture_rgba.empty() && + volume.imported_texture_rgba.size() >= + size_t(volume.imported_texture_width) * size_t(volume.imported_texture_height) * 4 && + volume.imported_texture_uv_valid.size() == triangle_count && + volume.imported_texture_uvs_per_face.size() >= triangle_count * 6; +} + +static bool has_imported_obj_material_payload(const ModelVolume &volume) +{ + return has_imported_vertex_color_payload(volume) || has_imported_obj_texture_payload(volume); +} + +static std::string imported_obj_texture_part_path(const Model &model, const ModelObject &object, const size_t volume_index) +{ + size_t object_index = 0; + for (; object_index < model.objects.size(); ++object_index) { + if (model.objects[object_index] == &object) + break; + } + + return (boost::format("3D/Texture/obj_texture_%1%_%2%.png") % (object_index + 1) % (volume_index + 1)).str(); +} + +static std::string model_relationships_part_path(const std::string &model_part_path) +{ + const boost::filesystem::path model_path(model_part_path); + const boost::filesystem::path rels_path = model_path.parent_path() / "_rels" / + boost::filesystem::path(model_path.filename().string() + ".rels"); + return rels_path.generic_string(); +} + +static int hex_digit_to_int(const char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + c - 'a'; + if (c >= 'A' && c <= 'F') + return 10 + c - 'A'; + return -1; +} + +static bool parse_3mf_color(const std::string &color, uint32_t &rgba) +{ + if (color.size() != 7 && color.size() != 9) + return false; + if (color.front() != '#') + return false; + + auto parse_byte = [&color](const size_t offset, uint32_t &out) { + const int hi = hex_digit_to_int(color[offset]); + const int lo = hex_digit_to_int(color[offset + 1]); + if (hi < 0 || lo < 0) + return false; + out = uint32_t((hi << 4) | lo); + return true; + }; + + uint32_t r = 0; + uint32_t g = 0; + uint32_t b = 0; + uint32_t a = 255; + if (!parse_byte(1, r) || !parse_byte(3, g) || !parse_byte(5, b)) + return false; + if (color.size() == 9 && !parse_byte(7, a)) + return false; + + rgba = (r << 24) | (g << 16) | (b << 8) | a; + return true; +} + +static std::string format_3mf_color(const uint32_t rgba) +{ + char buf[10]; + ::snprintf(buf, sizeof(buf), "#%02X%02X%02X%02X", + unsigned((rgba >> 24) & 0xFFu), + unsigned((rgba >> 16) & 0xFFu), + unsigned((rgba >> 8) & 0xFFu), + unsigned(rgba & 0xFFu)); + return buf; +} + +static std::vector parse_3mf_int_list(const std::string &text) +{ + std::vector values; + std::vector tokens; + boost::split(tokens, text, boost::is_any_of(" \t\r\n"), boost::token_compress_on); + values.reserve(tokens.size()); + for (const std::string &token : tokens) { + if (token.empty()) + continue; + + int value = 0; + const char *begin = token.c_str(); + const char *end = begin + token.size(); + if (boost::spirit::qi::parse(begin, end, boost::spirit::qi::int_, value) && begin == end) + values.emplace_back(value); + } + return values; +} + +struct JpegDecodeErrorManagerMemory +{ + jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +static void jpeg_decode_error_exit_memory(j_common_ptr cinfo) +{ + auto *err = reinterpret_cast(cinfo->err); + longjmp(err->setjmp_buffer, 1); +} + +static bool decode_jpeg_rgba_from_memory(const std::vector &encoded, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (encoded.empty()) + return false; + + jpeg_decompress_struct cinfo; + JpegDecodeErrorManagerMemory jerr; + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = jpeg_decode_error_exit_memory; + + if (setjmp(jerr.setjmp_buffer)) { + jpeg_destroy_decompress(&cinfo); + return false; + } + + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, + reinterpret_cast(encoded.data()), + static_cast(encoded.size())); + + if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { + jpeg_destroy_decompress(&cinfo); + return false; + } + + jpeg_start_decompress(&cinfo); + + const uint32_t width = cinfo.output_width; + const uint32_t height = cinfo.output_height; + const int components = cinfo.output_components; + if (width == 0 || height == 0 || components <= 0) { + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return false; + } + + out_rgba.assign(size_t(width) * size_t(height) * 4, uint8_t(255)); + JSAMPARRAY scanline = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, + JPOOL_IMAGE, + width * components, + 1); + + uint32_t y = 0; + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, scanline, 1); + const unsigned char *src = scanline[0]; + for (uint32_t x = 0; x < width; ++x) { + const size_t dst = (size_t(y) * size_t(width) + size_t(x)) * 4; + if (components >= 3) { + const size_t s = size_t(x) * size_t(components); + out_rgba[dst + 0] = src[s + 0]; + out_rgba[dst + 1] = src[s + 1]; + out_rgba[dst + 2] = src[s + 2]; + } else { + const unsigned char g = src[x]; + out_rgba[dst + 0] = g; + out_rgba[dst + 1] = g; + out_rgba[dst + 2] = g; + } + out_rgba[dst + 3] = 255; + } + ++y; + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + + out_width = width; + out_height = height; + return true; +} + +static bool decode_png_rgba_from_memory(const std::vector &encoded, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (encoded.empty()) + return false; + + png::ReadBuf rb{encoded.data(), encoded.size()}; + png::ImageColorscale img; + if (!png::decode_colored_png(rb, img)) + return false; + if (img.cols == 0 || img.rows == 0 || (img.bytes_per_pixel != 3 && img.bytes_per_pixel != 4)) + return false; + + const size_t row_stride = img.cols * size_t(img.bytes_per_pixel); + if (img.buf.size() < img.rows * row_stride) + return false; + + out_rgba.assign(img.rows * img.cols * 4, uint8_t(255)); + for (size_t y = 0; y < img.rows; ++y) { + const size_t src_row_off = y * row_stride; + const size_t dst_row_off = y * img.cols * 4; + for (size_t x = 0; x < img.cols; ++x) { + const size_t src = src_row_off + x * size_t(img.bytes_per_pixel); + const size_t dst = dst_row_off + x * 4; + out_rgba[dst + 0] = img.buf[src + 0]; + out_rgba[dst + 1] = img.buf[src + 1]; + out_rgba[dst + 2] = img.buf[src + 2]; + out_rgba[dst + 3] = (img.bytes_per_pixel == 4) ? img.buf[src + 3] : uint8_t(255); + } + } + + out_width = uint32_t(img.cols); + out_height = uint32_t(img.rows); + return true; +} + +static bool decode_texture_rgba_from_memory(const std::vector &encoded, + const std::string &content_type, + const std::string &path, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + const bool prefer_jpeg = boost::algorithm::iequals(content_type, "image/jpeg") || + boost::algorithm::iends_with(path, ".jpg") || + boost::algorithm::iends_with(path, ".jpeg"); + if (prefer_jpeg) + return decode_jpeg_rgba_from_memory(encoded, out_rgba, out_width, out_height); + + if (decode_png_rgba_from_memory(encoded, out_rgba, out_width, out_height)) + return true; + + return decode_jpeg_rgba_from_memory(encoded, out_rgba, out_width, out_height); +} + +static bool try_extract_file_from_archive(mz_zip_archive &archive, std::string path_in_zip, std::vector &out_data) +{ + out_data.clear(); + if (path_in_zip.empty()) + return false; + if (path_in_zip.front() == '/') + path_in_zip.erase(path_in_zip.begin()); + + int index = mz_zip_reader_locate_file(&archive, path_in_zip.c_str(), nullptr, 0); + if (index < 0) { + const std::string native_path = encode_path(path_in_zip.c_str()); + index = mz_zip_reader_locate_file(&archive, native_path.c_str(), nullptr, 0); + } + if (index < 0) + return false; + + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(&archive, index, &stat) || stat.m_uncomp_size == 0) + return false; + + out_data.resize(stat.m_uncomp_size); + return mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, out_data.data(), out_data.size(), 0) != 0; +} + +static void append_default_triangle_texture_data(std::vector &uv_valid, + std::vector &uvs_per_face) +{ + uv_valid.emplace_back(uint8_t(0)); + uvs_per_face.insert(uvs_per_face.end(), 6, 0.f); +} + +struct ThreeMfTriangleProperties +{ + bool has_color{false}; + std::array colors{{0, 0, 0}}; + bool has_texture{false}; + std::string texture_image_path; + std::string texture_image_content_type; + std::array uvs{{0.f, 0.f, 0.f, 0.f, 0.f, 0.f}}; +}; + +static int multi_property_index_or_default(const std::vector &pindices, const size_t pid_index) +{ + return pid_index < pindices.size() ? pindices[pid_index] : 0; +} + +static bool set_triangle_color_properties(ThreeMfTriangleProperties &properties, + const ThreeMfColorGroupResource &color_group, + const std::array &indices) +{ + for (const int index : indices) + if (index < 0 || size_t(index) >= color_group.colors.size()) + return false; + + properties.has_color = true; + properties.colors[0] = color_group.colors[size_t(indices[0])]; + properties.colors[1] = color_group.colors[size_t(indices[1])]; + properties.colors[2] = color_group.colors[size_t(indices[2])]; + return true; +} + +static bool set_triangle_texture_properties(ThreeMfTriangleProperties &properties, + const ThreeMfTexture2DGroupResource &texture_group, + const ThreeMfTexture2DResource &texture, + const std::array &indices) +{ + for (const int index : indices) + if (index < 0 || size_t(index) >= texture_group.coords.size()) + return false; + + properties.has_texture = true; + properties.texture_image_path = texture.path; + properties.texture_image_content_type = texture.content_type; + properties.uvs[0] = texture_group.coords[size_t(indices[0])].first; + properties.uvs[1] = texture_group.coords[size_t(indices[0])].second; + properties.uvs[2] = texture_group.coords[size_t(indices[1])].first; + properties.uvs[3] = texture_group.coords[size_t(indices[1])].second; + properties.uvs[4] = texture_group.coords[size_t(indices[2])].first; + properties.uvs[5] = texture_group.coords[size_t(indices[2])].second; + return true; +} + +static ThreeMfTriangleProperties extract_triangle_material_properties( + const std::map &color_groups, + const std::map &texture_groups, + const std::map &textures, + const std::map &multi_properties, + const char **attributes, + const unsigned int num_attributes) +{ + ThreeMfTriangleProperties properties; + + const char *pid_text = bbs_get_attribute_value_charptr(attributes, num_attributes, PID_ATTR); + if (pid_text == nullptr) + return properties; + + const int pid = bbs_get_attribute_value_int(attributes, num_attributes, PID_ATTR); + const char *p1_text = bbs_get_attribute_value_charptr(attributes, num_attributes, P1_ATTR); + const char *p2_text = bbs_get_attribute_value_charptr(attributes, num_attributes, P2_ATTR); + const char *p3_text = bbs_get_attribute_value_charptr(attributes, num_attributes, P3_ATTR); + const int p1 = p1_text != nullptr ? bbs_get_attribute_value_int(attributes, num_attributes, P1_ATTR) : 0; + const int p2 = p2_text != nullptr ? bbs_get_attribute_value_int(attributes, num_attributes, P2_ATTR) : p1; + const int p3 = p3_text != nullptr ? bbs_get_attribute_value_int(attributes, num_attributes, P3_ATTR) : p1; + if (p1 < 0 || p2 < 0 || p3 < 0) + return properties; + + if (const auto color_group_it = color_groups.find(pid); color_group_it != color_groups.end()) { + set_triangle_color_properties(properties, color_group_it->second, {{p1, p2, p3}}); + return properties; + } + + if (const auto texture_group_it = texture_groups.find(pid); texture_group_it != texture_groups.end()) { + if (const auto texture_it = textures.find(texture_group_it->second.tex_id); texture_it != textures.end()) + set_triangle_texture_properties(properties, texture_group_it->second, texture_it->second, {{p1, p2, p3}}); + return properties; + } + + const auto multi_properties_it = multi_properties.find(pid); + if (multi_properties_it == multi_properties.end()) + return properties; + + const ThreeMfMultiPropertiesResource &multi = multi_properties_it->second; + const std::array multi_indices{{p1, p2, p3}}; + for (const int multi_index : multi_indices) + if (multi_index < 0 || size_t(multi_index) >= multi.pindices.size()) + return properties; + + for (size_t pid_index = 0; pid_index < multi.pids.size(); ++pid_index) { + const int property_group_id = multi.pids[pid_index]; + std::array property_indices{{ + multi_property_index_or_default(multi.pindices[size_t(p1)], pid_index), + multi_property_index_or_default(multi.pindices[size_t(p2)], pid_index), + multi_property_index_or_default(multi.pindices[size_t(p3)], pid_index) + }}; + + if (const auto color_group_it = color_groups.find(property_group_id); color_group_it != color_groups.end()) { + set_triangle_color_properties(properties, color_group_it->second, property_indices); + } else if (const auto texture_group_it = texture_groups.find(property_group_id); texture_group_it != texture_groups.end()) { + if (const auto texture_it = textures.find(texture_group_it->second.tex_id); texture_it != textures.end()) + set_triangle_texture_properties(properties, texture_group_it->second, texture_it->second, property_indices); + } + } + + return properties; +} + +static void append_triangle_material_data(std::vector &uv_valid, + std::vector &uvs_per_face, + std::string &image_path, + std::string &image_content_type, + bool &multiple_texture_images, + std::vector &vertex_colors, + std::vector &vertex_color_valid, + const size_t vertex_count, + const Vec3i32 &triangle, + const std::map &color_groups, + const std::map &texture_groups, + const std::map &textures, + const std::map &multi_properties, + const char **attributes, + const unsigned int num_attributes) +{ + append_default_triangle_texture_data(uv_valid, uvs_per_face); + + const ThreeMfTriangleProperties properties = extract_triangle_material_properties( + color_groups, texture_groups, textures, multi_properties, attributes, num_attributes); + + if (properties.has_texture) { + if (!image_path.empty() && image_path != properties.texture_image_path) + multiple_texture_images = true; + + if (image_path.empty()) { + image_path = properties.texture_image_path; + image_content_type = properties.texture_image_content_type; + } + + uv_valid.back() = uint8_t(1); + const size_t base = uvs_per_face.size() - 6; + for (size_t i = 0; i < properties.uvs.size(); ++i) + uvs_per_face[base + i] = properties.uvs[i]; + } + + if (properties.has_color && vertex_count > 0) { + if (vertex_colors.size() != vertex_count) + vertex_colors.assign(vertex_count, 0u); + if (vertex_color_valid.size() != vertex_count) + vertex_color_valid.assign(vertex_count, uint8_t(0)); + + for (size_t corner = 0; corner < 3; ++corner) { + const int vertex_index = triangle[int(corner)]; + if (vertex_index < 0 || size_t(vertex_index) >= vertex_count) + continue; + vertex_colors[size_t(vertex_index)] = properties.colors[corner]; + vertex_color_valid[size_t(vertex_index)] = uint8_t(1); + } + } +} + // Base class with error messages management class _BBS_3MF_Base { @@ -725,6 +1277,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::vector fuzzy_skin; // BBS std::vector face_properties; + std::vector texture_uvs_per_face; + std::vector texture_uv_valid; + std::string texture_image_path; + std::string texture_image_content_type; + bool texture_uses_multiple_images{false}; + std::vector vertex_colors_rgba; + std::vector vertex_color_valid; bool empty() { return vertices.empty() || triangles.empty(); } @@ -734,6 +1293,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::swap(triangles, o.triangles); std::swap(custom_supports, o.custom_supports); std::swap(custom_seam, o.custom_seam); + std::swap(face_properties, o.face_properties); + std::swap(texture_uvs_per_face, o.texture_uvs_per_face); + std::swap(texture_uv_valid, o.texture_uv_valid); + std::swap(texture_image_path, o.texture_image_path); + std::swap(texture_image_content_type, o.texture_image_content_type); + std::swap(texture_uses_multiple_images, o.texture_uses_multiple_images); + std::swap(vertex_colors_rgba, o.vertex_colors_rgba); + std::swap(vertex_color_valid, o.vertex_color_valid); } void reset() { @@ -743,6 +1310,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) custom_seam.clear(); mmu_segmentation.clear(); fuzzy_skin.clear(); + face_properties.clear(); + texture_uvs_per_face.clear(); + texture_uv_valid.clear(); + texture_image_path.clear(); + texture_image_content_type.clear(); + texture_uses_multiple_images = false; + vertex_colors_rgba.clear(); + vertex_color_valid.clear(); } }; @@ -898,7 +1473,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::string obj_curr_characters; float object_unit_factor; int object_current_color_group{-1}; + int object_current_texture_group{-1}; + int object_current_multi_properties{-1}; std::map object_group_id_to_color; + std::map object_color_groups; + std::map object_texture_resources; + std::map object_texture_groups; + std::map object_multi_properties; bool is_bbl_3mf { false }; ObjectImporter(_BBS_3MF_Importer *importer, std::string file_path, std::string obj_path) @@ -988,6 +1569,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _handle_object_start_color(const char **attributes, unsigned int num_attributes); bool _handle_object_end_color(); + bool _handle_object_start_texture_2d(const char **attributes, unsigned int num_attributes); + bool _handle_object_end_texture_2d(); + + bool _handle_object_start_texture_2d_group(const char **attributes, unsigned int num_attributes); + bool _handle_object_end_texture_2d_group(); + + bool _handle_object_start_tex2coord(const char **attributes, unsigned int num_attributes); + bool _handle_object_end_tex2coord(); + + bool _handle_object_start_multi_properties(const char **attributes, unsigned int num_attributes); + bool _handle_object_end_multi_properties(); + + bool _handle_object_start_multi(const char **attributes, unsigned int num_attributes); + bool _handle_object_end_multi(); + bool _handle_object_start_mesh(const char** attributes, unsigned int num_attributes); bool _handle_object_end_mesh(); @@ -1089,6 +1685,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::vector m_object_importers; std::map m_shared_meshes; + std::map m_volume_subobject_ids; + std::map m_standard_texture_sources; + int m_current_texture_group{-1}; + int m_current_multi_properties{-1}; + std::map m_color_groups; + std::map m_texture_resources; + std::map m_texture_groups; + std::map m_multi_properties; //BBS: plater related structures bool m_is_bbl_3mf { false }; @@ -1177,6 +1781,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _handle_start_color(const char **attributes, unsigned int num_attributes); bool _handle_end_color(); + bool _handle_start_texture_2d(const char **attributes, unsigned int num_attributes); + bool _handle_end_texture_2d(); + + bool _handle_start_texture_2d_group(const char **attributes, unsigned int num_attributes); + bool _handle_end_texture_2d_group(); + + bool _handle_start_tex2coord(const char **attributes, unsigned int num_attributes); + bool _handle_end_tex2coord(); + + bool _handle_start_multi_properties(const char **attributes, unsigned int num_attributes); + bool _handle_end_multi_properties(); + + bool _handle_start_multi(const char **attributes, unsigned int num_attributes); + bool _handle_end_multi(); + bool _handle_start_mesh(const char** attributes, unsigned int num_attributes); bool _handle_end_mesh(); @@ -1261,6 +1880,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects); bool _generate_volumes_new(ModelObject& object, const std::vector &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); + void _restore_imported_obj_textures_from_archive(mz_zip_archive &archive); //bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); // callbacks to parse the .model file @@ -1299,6 +1919,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_index_paths.clear(); m_objects.clear(); m_instances.clear(); + m_volume_subobject_ids.clear(); + m_standard_texture_sources.clear(); + m_color_groups.clear(); + m_texture_resources.clear(); + m_texture_groups.clear(); + m_multi_properties.clear(); + m_current_texture_group = -1; + m_current_multi_properties = -1; m_objects_metadata.clear(); m_curr_metadata_name.clear(); m_curr_characters.clear(); @@ -1336,6 +1964,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //m_objects_aliases.clear(); m_instances.clear(); //m_geometries.clear(); + m_volume_subobject_ids.clear(); + m_standard_texture_sources.clear(); + m_color_groups.clear(); + m_texture_resources.clear(); + m_texture_groups.clear(); + m_multi_properties.clear(); + m_current_texture_group = -1; + m_current_multi_properties = -1; m_curr_config.object_id = -1; m_curr_config.volume_id = -1; m_objects_metadata.clear(); @@ -1475,6 +2111,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //extract model files m_model = &model; + m_volume_subobject_ids.clear(); + m_standard_texture_sources.clear(); + m_color_groups.clear(); + m_texture_resources.clear(); + m_texture_groups.clear(); + m_multi_properties.clear(); + m_current_texture_group = -1; + m_current_multi_properties = -1; if (!_extract_from_archive(archive, m_start_part_path, [this] (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) { return _extract_model_from_archive(archive, stat); })) { @@ -1592,6 +2236,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) it++; } + _restore_imported_obj_textures_from_archive(archive); lock.close(); return true; @@ -1754,6 +2399,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_current_objects.insert({ std::move(obj.first), std::move(obj.second)}); for (auto group_color : obj_importer->object_group_id_to_color) m_group_id_to_color.insert(std::move(group_color)); + for (auto color_group : obj_importer->object_color_groups) + m_color_groups.insert(std::move(color_group)); + for (auto texture_resource : obj_importer->object_texture_resources) + m_texture_resources.insert(std::move(texture_resource)); + for (auto texture_group : obj_importer->object_texture_groups) + m_texture_groups.insert(std::move(texture_group)); + for (auto multi_properties : obj_importer->object_multi_properties) + m_multi_properties.insert(std::move(multi_properties)); delete obj_importer; } @@ -1934,6 +2587,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } } + _restore_imported_obj_textures_from_archive(archive); lock.close(); if (!m_is_bbl_3mf) { @@ -2172,7 +2826,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (extruder_opt != nullptr) extruder_id = extruder_opt->getInt(); - if (extruder_id == 0 || extruder_id > max_filament_id) + if (extruder_id == 0 || + (extruder_id > max_filament_id && + !is_texture_mapping_virtual_filament_id(config, extruder_id, size_t(max_filament_id)))) mo->config.set_key_value("extruder", new ConfigOptionInt(1)); if (mo->volumes.size() == 1) { @@ -2186,7 +2842,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (vol_extruder_opt->getInt() == 0) mv->config.erase("extruder"); - else if (vol_extruder_opt->getInt() > max_filament_id) + else if (vol_extruder_opt->getInt() > max_filament_id && + !is_texture_mapping_virtual_filament_id(config, vol_extruder_opt->getInt(), size_t(max_filament_id))) mv->config.set_key_value("extruder", new ConfigOptionInt(1)); } } @@ -3244,6 +3901,16 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_start_color_group(attributes, num_attributes); else if (::strcmp(COLOR_TAG, name) == 0) res = _handle_start_color(attributes, num_attributes); + else if (::strcmp(TEXTURE_2D_TAG, name) == 0) + res = _handle_start_texture_2d(attributes, num_attributes); + else if (::strcmp(TEXTURE_2D_GROUP_TAG, name) == 0) + res = _handle_start_texture_2d_group(attributes, num_attributes); + else if (::strcmp(TEX2COORD_TAG, name) == 0) + res = _handle_start_tex2coord(attributes, num_attributes); + else if (::strcmp(MULTI_PROPERTIES_TAG, name) == 0) + res = _handle_start_multi_properties(attributes, num_attributes); + else if (::strcmp(MULTI_TAG, name) == 0) + res = _handle_start_multi(attributes, num_attributes); else if (::strcmp(MESH_TAG, name) == 0) res = _handle_start_mesh(attributes, num_attributes); else if (::strcmp(VERTICES_TAG, name) == 0) @@ -3286,6 +3953,16 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_end_color_group(); else if (::strcmp(COLOR_TAG, name) == 0) res = _handle_end_color(); + else if (::strcmp(TEXTURE_2D_TAG, name) == 0) + res = _handle_end_texture_2d(); + else if (::strcmp(TEXTURE_2D_GROUP_TAG, name) == 0) + res = _handle_end_texture_2d_group(); + else if (::strcmp(TEX2COORD_TAG, name) == 0) + res = _handle_end_tex2coord(); + else if (::strcmp(MULTI_PROPERTIES_TAG, name) == 0) + res = _handle_end_multi_properties(); + else if (::strcmp(MULTI_TAG, name) == 0) + res = _handle_end_multi(); else if (::strcmp(MESH_TAG, name) == 0) res = _handle_end_mesh(); else if (::strcmp(VERTICES_TAG, name) == 0) @@ -3598,12 +4275,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _BBS_3MF_Importer::_handle_start_color_group(const char **attributes, unsigned int num_attributes) { m_current_color_group = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (m_current_color_group > 0) + m_color_groups[m_current_color_group] = ThreeMfColorGroupResource(); return true; } bool _BBS_3MF_Importer::_handle_end_color_group() { - // do nothing + m_current_color_group = -1; return true; } @@ -3611,6 +4290,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { std::string color = bbs_get_attribute_value_string(attributes, num_attributes, COLOR_ATTR); m_group_id_to_color[m_current_color_group] = color; + + uint32_t rgba = 0; + if (m_current_color_group > 0 && parse_3mf_color(color, rgba)) + m_color_groups[m_current_color_group].colors.emplace_back(rgba); return true; } @@ -3620,6 +4303,95 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_3MF_Importer::_handle_start_texture_2d(const char **attributes, unsigned int num_attributes) + { + const int id = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (id <= 0) + return true; + + ThreeMfTexture2DResource resource; + resource.path = xml_unescape(bbs_get_attribute_value_string(attributes, num_attributes, PATH_ATTR)); + resource.content_type = bbs_get_attribute_value_string(attributes, num_attributes, CONTENTTYPE_ATTR); + m_texture_resources[id] = std::move(resource); + return true; + } + + bool _BBS_3MF_Importer::_handle_end_texture_2d() + { + return true; + } + + bool _BBS_3MF_Importer::_handle_start_texture_2d_group(const char **attributes, unsigned int num_attributes) + { + const int id = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (id <= 0) + return true; + + ThreeMfTexture2DGroupResource group; + group.tex_id = bbs_get_attribute_value_int(attributes, num_attributes, TEXID_ATTR); + m_texture_groups[id] = std::move(group); + m_current_texture_group = id; + return true; + } + + bool _BBS_3MF_Importer::_handle_end_texture_2d_group() + { + m_current_texture_group = -1; + return true; + } + + bool _BBS_3MF_Importer::_handle_start_tex2coord(const char **attributes, unsigned int num_attributes) + { + const auto group_it = m_texture_groups.find(m_current_texture_group); + if (group_it == m_texture_groups.end()) + return true; + + group_it->second.coords.emplace_back( + bbs_get_attribute_value_float(attributes, num_attributes, U_ATTR), + bbs_get_attribute_value_float(attributes, num_attributes, V_ATTR)); + return true; + } + + bool _BBS_3MF_Importer::_handle_end_tex2coord() + { + return true; + } + + bool _BBS_3MF_Importer::_handle_start_multi_properties(const char **attributes, unsigned int num_attributes) + { + const int id = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (id <= 0) + return true; + + ThreeMfMultiPropertiesResource resource; + resource.pids = parse_3mf_int_list(bbs_get_attribute_value_string(attributes, num_attributes, PIDS_ATTR)); + m_multi_properties[id] = std::move(resource); + m_current_multi_properties = id; + return true; + } + + bool _BBS_3MF_Importer::_handle_end_multi_properties() + { + m_current_multi_properties = -1; + return true; + } + + bool _BBS_3MF_Importer::_handle_start_multi(const char **attributes, unsigned int num_attributes) + { + const auto multi_it = m_multi_properties.find(m_current_multi_properties); + if (multi_it == m_multi_properties.end()) + return true; + + multi_it->second.pindices.emplace_back( + parse_3mf_int_list(bbs_get_attribute_value_string(attributes, num_attributes, PINDICES_ATTR))); + return true; + } + + bool _BBS_3MF_Importer::_handle_end_multi() + { + return true; + } + bool _BBS_3MF_Importer::_handle_start_mesh(const char** attributes, unsigned int num_attributes) { // reset current geometry @@ -3703,6 +4475,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); // BBS m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); + append_triangle_material_data(m_curr_object->geometry.texture_uv_valid, + m_curr_object->geometry.texture_uvs_per_face, + m_curr_object->geometry.texture_image_path, + m_curr_object->geometry.texture_image_content_type, + m_curr_object->geometry.texture_uses_multiple_images, + m_curr_object->geometry.vertex_colors_rgba, + m_curr_object->geometry.vertex_color_valid, + m_curr_object->geometry.vertices.size(), + m_curr_object->geometry.triangles.back(), + m_color_groups, + m_texture_groups, + m_texture_resources, + m_multi_properties, + attributes, + num_attributes); } return true; } @@ -4901,6 +5688,30 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) volume->translate(shift); } + m_volume_subobject_ids[volume] = sub_object->id; + + if (!sub_object->geometry.texture_image_path.empty() && + !sub_object->geometry.texture_uses_multiple_images && + sub_object->geometry.texture_uv_valid.size() == triangles_count && + sub_object->geometry.texture_uvs_per_face.size() >= triangles_count * 6) { + PendingThreeMfImportedTexture texture_source; + texture_source.image_file = sub_object->geometry.texture_image_path; + texture_source.image_content_type = sub_object->geometry.texture_image_content_type; + texture_source.uv_valid = sub_object->geometry.texture_uv_valid; + texture_source.uvs_per_face.assign(sub_object->geometry.texture_uvs_per_face.begin(), + sub_object->geometry.texture_uvs_per_face.begin() + triangles_count * 6); + m_standard_texture_sources[volume] = std::move(texture_source); + } + + const size_t vertices_count = volume->mesh().its.vertices.size(); + if (sub_object->geometry.vertex_colors_rgba.size() == vertices_count && + sub_object->geometry.vertex_color_valid.size() == vertices_count && + std::all_of(sub_object->geometry.vertex_color_valid.begin(), + sub_object->geometry.vertex_color_valid.end(), + [](const uint8_t valid) { return valid != 0; })) { + volume->imported_vertex_colors_rgba = sub_object->geometry.vertex_colors_rgba; + } + // recreate custom supports, seam and mmu segmentation from previously loaded attribute { volume->supported_facets.reserve(triangles_count); @@ -4978,6 +5789,54 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + + void _BBS_3MF_Importer::_restore_imported_obj_textures_from_archive(mz_zip_archive &archive) + { + if (m_model == nullptr) + return; + + for (auto &standard_texture_entry : m_standard_texture_sources) { + ModelVolume *volume = standard_texture_entry.first; + if (volume == nullptr) + continue; + + PendingThreeMfImportedTexture &source = standard_texture_entry.second; + if (source.image_file.empty() || source.uv_valid.empty() || source.uvs_per_face.empty()) + continue; + + std::vector image_payload; + if (!try_extract_file_from_archive(archive, source.image_file, image_payload)) { + BOOST_LOG_TRIVIAL(warning) << "3MF texture2d payload missing for image='" << source.image_file << "'"; + continue; + } + + std::vector imported_rgba; + uint32_t imported_width = 0; + uint32_t imported_height = 0; + if (!decode_texture_rgba_from_memory(image_payload, + source.image_content_type, + source.image_file, + imported_rgba, + imported_width, + imported_height)) { + BOOST_LOG_TRIVIAL(warning) << "3MF texture2d image decode failed for image='" << source.image_file << "'"; + continue; + } + + const size_t triangle_count = volume->mesh().its.indices.size(); + if (source.uv_valid.size() != triangle_count || source.uvs_per_face.size() < triangle_count * 6) { + BOOST_LOG_TRIVIAL(warning) << "3MF texture2d UV payload triangle mismatch for image='" << source.image_file << "'"; + continue; + } + + volume->imported_texture_uv_valid = source.uv_valid; + volume->imported_texture_uvs_per_face.assign(source.uvs_per_face.begin(), + source.uvs_per_face.begin() + triangle_count * 6); + volume->imported_texture_rgba = std::move(imported_rgba); + volume->imported_texture_width = imported_width; + volume->imported_texture_height = imported_height; + } + } /* bool _BBS_3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) { @@ -5283,12 +6142,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_color_group(const char **attributes, unsigned int num_attributes) { object_current_color_group = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (object_current_color_group > 0) + object_color_groups[object_current_color_group] = ThreeMfColorGroupResource(); return true; } bool _BBS_3MF_Importer::ObjectImporter::_handle_object_end_color_group() { - // do nothing + object_current_color_group = -1; return true; } @@ -5296,6 +6157,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { std::string color = bbs_get_attribute_value_string(attributes, num_attributes, COLOR_ATTR); object_group_id_to_color[object_current_color_group] = color; + + uint32_t rgba = 0; + if (object_current_color_group > 0 && parse_3mf_color(color, rgba)) + object_color_groups[object_current_color_group].colors.emplace_back(rgba); return true; } @@ -5305,6 +6170,95 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_texture_2d(const char **attributes, unsigned int num_attributes) + { + const int id = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (id <= 0) + return true; + + ThreeMfTexture2DResource resource; + resource.path = xml_unescape(bbs_get_attribute_value_string(attributes, num_attributes, PATH_ATTR)); + resource.content_type = bbs_get_attribute_value_string(attributes, num_attributes, CONTENTTYPE_ATTR); + object_texture_resources[id] = std::move(resource); + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_end_texture_2d() + { + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_texture_2d_group(const char **attributes, unsigned int num_attributes) + { + const int id = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (id <= 0) + return true; + + ThreeMfTexture2DGroupResource group; + group.tex_id = bbs_get_attribute_value_int(attributes, num_attributes, TEXID_ATTR); + object_texture_groups[id] = std::move(group); + object_current_texture_group = id; + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_end_texture_2d_group() + { + object_current_texture_group = -1; + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_tex2coord(const char **attributes, unsigned int num_attributes) + { + const auto group_it = object_texture_groups.find(object_current_texture_group); + if (group_it == object_texture_groups.end()) + return true; + + group_it->second.coords.emplace_back( + bbs_get_attribute_value_float(attributes, num_attributes, U_ATTR), + bbs_get_attribute_value_float(attributes, num_attributes, V_ATTR)); + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_end_tex2coord() + { + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_multi_properties(const char **attributes, unsigned int num_attributes) + { + const int id = bbs_get_attribute_value_int(attributes, num_attributes, ID_ATTR); + if (id <= 0) + return true; + + ThreeMfMultiPropertiesResource resource; + resource.pids = parse_3mf_int_list(bbs_get_attribute_value_string(attributes, num_attributes, PIDS_ATTR)); + object_multi_properties[id] = std::move(resource); + object_current_multi_properties = id; + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_end_multi_properties() + { + object_current_multi_properties = -1; + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_multi(const char **attributes, unsigned int num_attributes) + { + const auto multi_it = object_multi_properties.find(object_current_multi_properties); + if (multi_it == object_multi_properties.end()) + return true; + + multi_it->second.pindices.emplace_back( + parse_3mf_int_list(bbs_get_attribute_value_string(attributes, num_attributes, PINDICES_ATTR))); + return true; + } + + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_end_multi() + { + return true; + } + bool _BBS_3MF_Importer::ObjectImporter::_handle_object_start_mesh(const char** attributes, unsigned int num_attributes) { // reset current geometry @@ -5388,6 +6342,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); // BBS current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); + append_triangle_material_data(current_object->geometry.texture_uv_valid, + current_object->geometry.texture_uvs_per_face, + current_object->geometry.texture_image_path, + current_object->geometry.texture_image_content_type, + current_object->geometry.texture_uses_multiple_images, + current_object->geometry.vertex_colors_rgba, + current_object->geometry.vertex_color_valid, + current_object->geometry.vertices.size(), + current_object->geometry.triangles.back(), + object_color_groups, + object_texture_groups, + object_texture_resources, + object_multi_properties, + attributes, + num_attributes); } return true; } @@ -5478,6 +6447,16 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_object_start_color_group(attributes, num_attributes); else if (::strcmp(COLOR_TAG, name) == 0) res = _handle_object_start_color(attributes, num_attributes); + else if (::strcmp(TEXTURE_2D_TAG, name) == 0) + res = _handle_object_start_texture_2d(attributes, num_attributes); + else if (::strcmp(TEXTURE_2D_GROUP_TAG, name) == 0) + res = _handle_object_start_texture_2d_group(attributes, num_attributes); + else if (::strcmp(TEX2COORD_TAG, name) == 0) + res = _handle_object_start_tex2coord(attributes, num_attributes); + else if (::strcmp(MULTI_PROPERTIES_TAG, name) == 0) + res = _handle_object_start_multi_properties(attributes, num_attributes); + else if (::strcmp(MULTI_TAG, name) == 0) + res = _handle_object_start_multi(attributes, num_attributes); else if (::strcmp(MESH_TAG, name) == 0) res = _handle_object_start_mesh(attributes, num_attributes); else if (::strcmp(VERTICES_TAG, name) == 0) @@ -5516,6 +6495,16 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_object_end_color_group(); else if (::strcmp(COLOR_TAG, name) == 0) res = _handle_object_end_color(); + else if (::strcmp(TEXTURE_2D_TAG, name) == 0) + res = _handle_object_end_texture_2d(); + else if (::strcmp(TEXTURE_2D_GROUP_TAG, name) == 0) + res = _handle_object_end_texture_2d_group(); + else if (::strcmp(TEX2COORD_TAG, name) == 0) + res = _handle_object_end_tex2coord(); + else if (::strcmp(MULTI_PROPERTIES_TAG, name) == 0) + res = _handle_object_end_multi_properties(); + else if (::strcmp(MULTI_TAG, name) == 0) + res = _handle_object_end_multi(); else if (::strcmp(MESH_TAG, name) == 0) res = _handle_object_end_mesh(); else if (::strcmp(VERTICES_TAG, name) == 0) @@ -5677,6 +6666,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::string sub_path; bool share_mesh = false; VolumeToObjectIDMap volumes_objectID; + VolumeToThreeMfExportTextureMap volume_texture_resources; }; typedef std::vector BuildItemsList; @@ -5726,7 +6716,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _add_file_to_archive(mz_zip_archive& archive, const std::string & path_in_zip, const std::string & file_path); - bool _add_content_types_file_to_archive(mz_zip_archive& archive); + bool _add_content_types_file_to_archive(mz_zip_archive& archive, const Model& model); bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data, const char* local_path, int index, bool generate_small_thumbnail = false); bool _add_calibration_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data, int index); @@ -5934,7 +6924,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // Adds content types file ("[Content_Types].xml";). // The content of this file is the same for each OrcaSlicer 3mf. - if (!_add_content_types_file_to_archive(archive)) { + if (!_add_content_types_file_to_archive(archive, model)) { return false; } @@ -6364,7 +7354,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return result; } - bool _BBS_3MF_Exporter::_add_content_types_file_to_archive(mz_zip_archive& archive) + bool _BBS_3MF_Exporter::_add_content_types_file_to_archive(mz_zip_archive& archive, const Model& model) { std::stringstream stream; stream << "\n"; @@ -6372,7 +7362,27 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " \n"; stream << " \n"; stream << " \n"; + stream << " \n"; stream << " \n"; + + if (!m_skip_model) { + std::map texture_parts; + for (const ModelObject *object : model.objects) { + if (object == nullptr) + continue; + for (size_t volume_index = 0; volume_index < object->volumes.size(); ++volume_index) { + const ModelVolume *volume = object->volumes[volume_index]; + if (volume == nullptr || !has_imported_obj_texture_payload(*volume)) + continue; + texture_parts[imported_obj_texture_part_path(model, *object, volume_index)] = true; + } + } + + for (const auto &texture_part : texture_parts) + stream << " \n"; + } + stream << ""; std::string out = stream.str(); @@ -6583,6 +7593,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { bool sub_model = !objects_data.empty(); bool write_object = sub_model || !m_split_model; + bool has_materials_extension = false; + if (!m_skip_model && write_object) { + for (ModelObject *obj : model.objects) { + if (sub_model && obj != objects_data.begin()->second.object) + continue; + if (obj == nullptr) + continue; + for (ModelVolume *volume : obj->volumes) { + if (volume != nullptr && has_imported_obj_material_payload(*volume)) { + has_materials_extension = true; + break; + } + } + if (has_materials_extension) + break; + } + } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", filename %1%, m_split_model %2%, sub_model %3%")%filename % m_split_model % sub_model; @@ -6617,8 +7644,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) reset_stream(stream); stream << "\n"; stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:BambuStudio=\"http://schemas.bambulab.com/package/2021\""; + if (has_materials_extension) + stream << " xmlns:m=\"" << MATERIALS_NAMESPACE << "\""; if (m_production_ext) - stream << " xmlns:p=\"http://schemas.microsoft.com/3dmanufacturing/production/2015/06\" requiredextensions=\"p\""; + stream << " xmlns:p=\"http://schemas.microsoft.com/3dmanufacturing/production/2015/06\""; + if (m_production_ext || has_materials_extension) { + stream << " requiredextensions=\""; + if (m_production_ext) + stream << "p"; + if (m_production_ext && has_materials_extension) + stream << " "; + if (has_materials_extension) + stream << "m"; + stream << "\""; + } stream << ">\n"; std::string origin; @@ -6728,6 +7767,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // Therefore the list of object_ids here may not be continuous. unsigned int object_id = 1; unsigned int object_index = 0; + unsigned int next_texture_resource_id = 1000000000u; bool cb_cancel = false; std::vector object_paths; @@ -6774,7 +7814,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if ((shared_volume->supported_facets.equals(volume->supported_facets)) && (shared_volume->seam_facets.equals(volume->seam_facets)) && (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets)) - && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))) + && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)) + && (shared_volume->imported_vertex_colors_rgba == volume->imported_vertex_colors_rgba) + && (shared_volume->imported_texture_uvs_per_face == volume->imported_texture_uvs_per_face) + && (shared_volume->imported_texture_uv_valid == volume->imported_texture_uv_valid) + && (shared_volume->imported_texture_rgba == volume->imported_texture_rgba) + && (shared_volume->imported_texture_width == volume->imported_texture_width) + && (shared_volume->imported_texture_height == volume->imported_texture_height)) { auto data = iter->second.first; const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}}); @@ -6795,9 +7841,144 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) object_data.object_id = object_id; } + if (write_object) { + auto &texture_resources = object_it->second.volume_texture_resources; + texture_resources.clear(); + for (size_t volume_index = 0; volume_index < obj->volumes.size(); ++volume_index) { + ModelVolume *volume = obj->volumes[volume_index]; + if (volume == nullptr || !has_imported_obj_material_payload(*volume)) + continue; + + const auto volume_id_it = object_it->second.volumes_objectID.find(volume); + if (volume_id_it != object_it->second.volumes_objectID.end() && m_share_mesh && volume_id_it->second == 0) + continue; + + const bool has_vertex_colors = has_imported_vertex_color_payload(*volume); + const bool has_texture = has_imported_obj_texture_payload(*volume); + ThreeMfExportTextureResource texture_resource; + if (has_vertex_colors) + texture_resource.color_group_id = int(next_texture_resource_id++); + if (has_texture) { + texture_resource.texture_id = int(next_texture_resource_id++); + texture_resource.texture_group_id = int(next_texture_resource_id++); + texture_resource.texture_part_path = imported_obj_texture_part_path(model, *obj, volume_index); + } + if (has_vertex_colors && has_texture) + texture_resource.multi_properties_id = int(next_texture_resource_id++); + + const size_t triangle_count = volume->mesh().its.indices.size(); + if (has_texture) { + texture_resource.triangle_texcoord_starts.assign(triangle_count, std::numeric_limits::max()); + uint32_t next_texcoord_index = 0; + for (size_t triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { + if (triangle_index >= volume->imported_texture_uv_valid.size() || !volume->imported_texture_uv_valid[triangle_index]) + continue; + texture_resource.triangle_texcoord_starts[triangle_index] = next_texcoord_index; + next_texcoord_index += 3; + } + } + + texture_resources[volume] = std::move(texture_resource); + } + } + if (m_skip_model) continue; if (write_object) { + if (!object_it->second.volume_texture_resources.empty()) { + std::stringstream resource_stream; + reset_stream(resource_stream); + for (size_t volume_index = 0; volume_index < obj->volumes.size(); ++volume_index) { + const ModelVolume *volume = obj->volumes[volume_index]; + if (volume == nullptr) + continue; + + const auto texture_it = object_it->second.volume_texture_resources.find(volume); + if (texture_it == object_it->second.volume_texture_resources.end()) + continue; + + const ThreeMfExportTextureResource &texture_resource = texture_it->second; + const indexed_triangle_set &its = volume->mesh().its; + const bool has_vertex_colors = texture_resource.color_group_id > 0; + const bool has_texture = texture_resource.texture_id > 0 && texture_resource.texture_group_id > 0; + const bool has_multi_properties = texture_resource.multi_properties_id > 0; + + if (has_vertex_colors) { + resource_stream << " <" << COLOR_GROUP_TAG + << " " << ID_ATTR << "=\"" << texture_resource.color_group_id << "\">\n"; + for (const uint32_t color : volume->imported_vertex_colors_rgba) + resource_stream << " <" << COLOR_TAG << " " << COLOR_ATTR << "=\"" + << format_3mf_color(color) << "\"/>\n"; + resource_stream << " \n"; + } + + if (has_texture) { + resource_stream << " <" << TEXTURE_2D_TAG + << " " << ID_ATTR << "=\"" << texture_resource.texture_id << "\"" + << " " << PATH_ATTR << "=\"/" << xml_escape(texture_resource.texture_part_path) << "\"" + << " " << CONTENTTYPE_ATTR << "=\"image/png\"/>\n"; + + resource_stream << " <" << TEXTURE_2D_GROUP_TAG + << " " << ID_ATTR << "=\"" << texture_resource.texture_group_id << "\"" + << " " << TEXID_ATTR << "=\"" << texture_resource.texture_id << "\">\n"; + + const size_t triangle_count = its.indices.size(); + for (size_t triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { + if (triangle_index >= volume->imported_texture_uv_valid.size() || !volume->imported_texture_uv_valid[triangle_index]) + continue; + const size_t uv_base = triangle_index * 6; + if (uv_base + 5 >= volume->imported_texture_uvs_per_face.size()) + continue; + + resource_stream << " <" << TEX2COORD_TAG << " " << U_ATTR << "=\"" + << volume->imported_texture_uvs_per_face[uv_base + 0] << "\" " + << V_ATTR << "=\"" << volume->imported_texture_uvs_per_face[uv_base + 1] << "\"/>\n"; + resource_stream << " <" << TEX2COORD_TAG << " " << U_ATTR << "=\"" + << volume->imported_texture_uvs_per_face[uv_base + 2] << "\" " + << V_ATTR << "=\"" << volume->imported_texture_uvs_per_face[uv_base + 3] << "\"/>\n"; + resource_stream << " <" << TEX2COORD_TAG << " " << U_ATTR << "=\"" + << volume->imported_texture_uvs_per_face[uv_base + 4] << "\" " + << V_ATTR << "=\"" << volume->imported_texture_uvs_per_face[uv_base + 5] << "\"/>\n"; + } + + resource_stream << " \n"; + } + + if (has_multi_properties) { + resource_stream << " <" << MULTI_PROPERTIES_TAG + << " " << ID_ATTR << "=\"" << texture_resource.multi_properties_id << "\"" + << " " << PIDS_ATTR << "=\"" << texture_resource.color_group_id << " " + << texture_resource.texture_group_id << "\">\n"; + + const size_t triangle_count = its.indices.size(); + for (size_t triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { + if (triangle_index >= texture_resource.triangle_texcoord_starts.size()) + continue; + const uint32_t texcoord_start = texture_resource.triangle_texcoord_starts[triangle_index]; + if (texcoord_start == std::numeric_limits::max()) + continue; + + const Vec3i32 &idx = its.indices[triangle_index]; + resource_stream << " <" << MULTI_TAG << " " << PINDICES_ATTR << "=\"" + << idx[0] << " " << texcoord_start + 0 << "\"/>\n"; + resource_stream << " <" << MULTI_TAG << " " << PINDICES_ATTR << "=\"" + << idx[1] << " " << texcoord_start + 1 << "\"/>\n"; + resource_stream << " <" << MULTI_TAG << " " << PINDICES_ATTR << "=\"" + << idx[2] << " " << texcoord_start + 2 << "\"/>\n"; + } + + resource_stream << " \n"; + } + } + + const std::string resource_buf = resource_stream.str(); + if (!resource_buf.empty() && !mz_zip_writer_add_staged_data(&context, resource_buf.data(), resource_buf.size())) { + add_error("Unable to add texture resources to model file"); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" << __LINE__ << boost::format(", Unable to add texture resources to model file\n"); + return false; + } + } + // Store geometry of all ModelVolumes contained in a single ModelObject into a single 3MF indexed triangle set object. // object_it->second.volumes_objectID will contain the offsets of the ModelVolumes in that single indexed triangle set. // object_id will be increased to point to the 1st instance of the next ModelObject. @@ -6856,6 +8037,65 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } } + if (!m_skip_model && write_object) { + std::map texture_targets; + for (const auto &object_entry : objects_data) { + const ObjectData &object_data = object_entry.second; + if (sub_model && object_data.object != objects_data.begin()->second.object) + continue; + for (const auto &texture_entry : object_data.volume_texture_resources) { + const ModelVolume *volume = texture_entry.first; + const ThreeMfExportTextureResource &texture_resource = texture_entry.second; + if (volume == nullptr || texture_resource.texture_part_path.empty()) + continue; + if (texture_targets.find(texture_resource.texture_part_path) != texture_targets.end()) + continue; + + size_t png_size = 0; + void *png_data = tdefl_write_image_to_png_file_in_memory_ex( + static_cast(volume->imported_texture_rgba.data()), + int(volume->imported_texture_width), + int(volume->imported_texture_height), + 4, + &png_size, + MZ_DEFAULT_COMPRESSION, + 1); + if (png_data == nullptr) { + add_error("Unable to encode standard 3MF texture image"); + return false; + } + + const bool added_png = mz_zip_writer_add_mem(&archive, + texture_resource.texture_part_path.c_str(), + png_data, + png_size, + MZ_NO_COMPRESSION); + mz_free(png_data); + if (!added_png) { + add_error("Unable to add standard 3MF texture image file to archive"); + return false; + } + + texture_targets[texture_resource.texture_part_path] = true; + } + } + + if (!texture_targets.empty()) { + std::vector texture_target_paths; + texture_target_paths.reserve(texture_targets.size()); + for (const auto &texture_target : texture_targets) + texture_target_paths.emplace_back(texture_target.first); + + const std::string model_part_path = sub_model ? filename : MODEL_FILE; + if (!_add_relationships_file_to_archive(archive, + model_relationships_part_path(model_part_path), + texture_target_paths, + { MODEL_TEXTURE_REL_TYPE })) { + return false; + } + } + } + if (m_skip_model || write_object) return true; // write model rels @@ -6863,30 +8103,64 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (!m_from_backup_save) { boost::mutex mutex; - tbb::parallel_for(tbb::blocked_range(0, objects_data.size(), 1), [this, &mutex, &model, objects = model.objects, &objects_data, &object_paths, main = &archive, project](const tbb::blocked_range& range) { + bool sub_models_added = true; + tbb::parallel_for(tbb::blocked_range(0, objects_data.size(), 1), + [this, &mutex, &model, objects = model.objects, &objects_data, &object_paths, main = &archive, project, &sub_models_added] + (const tbb::blocked_range& range) { for (size_t i = range.begin(); i < range.end(); ++i) { auto iter = objects_data.find(objects[i]); ObjectToObjectDataMap objects_data2; objects_data2.insert(*iter); - auto & object = *iter->second.object; - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - mz_zip_writer_init_heap(&archive, 0, 1024 * 1024); + mz_zip_archive sub_archive; + mz_zip_zero_struct(&sub_archive); + if (!mz_zip_writer_init_heap(&sub_archive, 0, 1024 * 1024)) { + boost::unique_lock l(mutex); + sub_models_added = false; + continue; + } + CNumericLocalesSetter locales_setter; - _add_model_file_to_archive(object_paths[i], archive, model, objects_data2, nullptr, project); + if (!_add_model_file_to_archive(object_paths[i], sub_archive, model, objects_data2, nullptr, project)) { + mz_zip_writer_end(&sub_archive); + boost::unique_lock l(mutex); + sub_models_added = false; + continue; + } iter->second = objects_data2.begin()->second; - void *ppBuf; size_t pSize; - mz_zip_writer_finalize_heap_archive(&archive, &ppBuf, &pSize); - mz_zip_writer_end(&archive); - mz_zip_zero_struct(&archive); - mz_zip_reader_init_mem(&archive, ppBuf, pSize, 0); + + void *ppBuf = nullptr; + size_t pSize = 0; + if (!mz_zip_writer_finalize_heap_archive(&sub_archive, &ppBuf, &pSize)) { + mz_zip_writer_end(&sub_archive); + boost::unique_lock l(mutex); + sub_models_added = false; + continue; + } + mz_zip_writer_end(&sub_archive); + mz_zip_zero_struct(&sub_archive); + if (!mz_zip_reader_init_mem(&sub_archive, ppBuf, pSize, 0)) { + mz_free(ppBuf); + boost::unique_lock l(mutex); + sub_models_added = false; + continue; + } { boost::unique_lock l(mutex); - mz_zip_writer_add_from_zip_reader(main, &archive, 0); + const mz_uint num_sub_entries = mz_zip_reader_get_num_files(&sub_archive); + for (mz_uint entry = 0; entry < num_sub_entries; ++entry) { + if (!mz_zip_writer_add_from_zip_reader(main, &sub_archive, entry)) + sub_models_added = false; + } } - mz_zip_reader_end(&archive); + mz_zip_reader_end(&sub_archive); + mz_free(ppBuf); } }); + + if (!sub_models_added) { + add_error("Unable to add split model files to archive"); + return false; + } } return true; @@ -7139,6 +8413,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //triangles_count += (int)its.indices.size(); //unsigned int last_triangle_id = triangles_count - 1; + const auto texture_resource_it = object_data.volume_texture_resources.find(volume); + for (int i = 0; i < int(its.indices.size()); ++ i) { { const Vec3i32 &idx = its.indices[i]; @@ -7202,6 +8478,57 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } } + if (texture_resource_it != object_data.volume_texture_resources.end()) { + const ThreeMfExportTextureResource &texture_resource = texture_resource_it->second; + const bool has_texture_for_triangle = + size_t(i) < texture_resource.triangle_texcoord_starts.size() && + texture_resource.triangle_texcoord_starts[size_t(i)] != std::numeric_limits::max(); + const uint32_t texcoord_start = has_texture_for_triangle ? + texture_resource.triangle_texcoord_starts[size_t(i)] : 0; + + int pid = -1; + int p1 = -1; + int p2 = -1; + int p3 = -1; + if (has_texture_for_triangle && texture_resource.multi_properties_id > 0) { + pid = texture_resource.multi_properties_id; + p1 = int(texcoord_start + 0); + p2 = int(texcoord_start + 1); + p3 = int(texcoord_start + 2); + } else if (has_texture_for_triangle && texture_resource.texture_group_id > 0) { + pid = texture_resource.texture_group_id; + p1 = int(texcoord_start + 0); + p2 = int(texcoord_start + 1); + p3 = int(texcoord_start + 2); + } else if (texture_resource.color_group_id > 0) { + const Vec3i32 &idx = its.indices[i]; + pid = texture_resource.color_group_id; + p1 = idx[is_left_handed ? 2 : 0]; + p2 = idx[1]; + p3 = idx[is_left_handed ? 0 : 2]; + } + + if (pid > 0 && p1 >= 0 && p2 >= 0 && p3 >= 0) { + output_buffer += " "; + output_buffer += PID_ATTR; + output_buffer += "=\""; + output_buffer += std::to_string(pid); + output_buffer += "\" "; + output_buffer += P1_ATTR; + output_buffer += "=\""; + output_buffer += std::to_string(p1); + output_buffer += "\" "; + output_buffer += P2_ATTR; + output_buffer += "=\""; + output_buffer += std::to_string(p2); + output_buffer += "\" "; + output_buffer += P3_ATTR; + output_buffer += "=\""; + output_buffer += std::to_string(p3); + output_buffer += "\""; + } + } + output_buffer += "/>\n"; if (! flush(output_buffer, false)) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 1ca8570763..3ae97a8fcd 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -14,18 +14,24 @@ #include "GCode/WipeTower.hpp" #include "ShortestPath.hpp" #include "Print.hpp" +#include "TextureMapping.hpp" #include "Utils.hpp" #include "ClipperUtils.hpp" #include "libslic3r.h" #include "LocalesUtils.hpp" #include "libslic3r/format.hpp" #include "Time.hpp" +#include "Color.hpp" #include "GCode/ExtrusionProcessor.hpp" +#include "filament_mixer.h" +#include #include #include #include #include +#include #include +#include #include #include #include @@ -2004,6 +2010,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu // BBS m_curr_print = print; + m_warned_texture_mapping_filament_count_mismatch = false; GCodeWriter::full_gcode_comment = print->config().gcode_comments; CNumericLocalesSetter locales_setter; @@ -4349,6 +4356,11 @@ LayerResult GCode::process_layer( // Either printing all copies of all objects, or just a single copy of a single object. assert(single_object_instance_idx == size_t(-1) || layers.size() == 1); + m_vertex_color_overhang_weight_field_cache.clear(); + ScopeGuard clear_vertex_color_weight_field_cache([this]() { + m_vertex_color_overhang_weight_field_cache.clear(); + }); + // First object, support and raft layer, if available. const Layer *object_layer = nullptr; const SupportLayer *support_layer = nullptr; @@ -5603,6 +5615,1462 @@ static std::unique_ptr calculate_layer_edge_grid(const Layer& la return out; } +static std::vector decode_offset_component_ids_for_gcode(const TextureMappingZone &zone, size_t num_physical) +{ + std::vector out; + for (const char c : zone.component_ids) { + if (c < '1' || c > '9') + continue; + const unsigned int id = unsigned(c - '0'); + if (id == 0 || id > num_physical) + continue; + if (std::find(out.begin(), out.end(), id) == out.end()) + out.emplace_back(id); + } + + if (out.empty()) { + if (zone.component_a >= 1 && zone.component_a <= num_physical) + out.emplace_back(zone.component_a); + if (zone.component_b >= 1 && zone.component_b <= num_physical && + std::find(out.begin(), out.end(), zone.component_b) == out.end()) { + out.emplace_back(zone.component_b); + } + } + return out; +} + +static float normalize_angle_deg_for_gcode(float angle) +{ + float normalized = std::fmod(angle, 360.f); + if (normalized < 0.f) + normalized += 360.f; + return normalized; +} + +static float angular_distance_deg_for_gcode(float a, float b) +{ + const float d = std::abs(normalize_angle_deg_for_gcode(a) - normalize_angle_deg_for_gcode(b)); + return std::min(d, 360.f - d); +} + +static float angular_distance_cw_deg_for_gcode(float from_deg, float to_deg) +{ + float d = normalize_angle_deg_for_gcode(to_deg) - normalize_angle_deg_for_gcode(from_deg); + if (d < 0.f) + d += 360.f; + return d; +} + +static float clamp01f_for_gcode(float v) +{ + if (!std::isfinite(v)) + return 0.f; + return std::clamp(v, 0.f, 1.f); +} + +static float extrusion_area_for_width_height_for_gcode(float width_mm, float height_mm) +{ + if (!std::isfinite(width_mm) || !std::isfinite(height_mm)) + return 1e-6f; + + const float safe_width = std::max(0.01f, width_mm); + const float safe_height = std::max(0.01f, height_mm); + const float area = safe_height * (safe_width - safe_height * (1.f - float(0.25 * PI))); + return std::max(1e-6f, area); +} + +static double flow_scale_for_target_width_for_gcode(float base_width_mm, float target_width_mm, float height_mm) +{ + if (!std::isfinite(base_width_mm) || !std::isfinite(target_width_mm) || !std::isfinite(height_mm)) + return 1.0; + + const float base_area = extrusion_area_for_width_height_for_gcode(base_width_mm, height_mm); + const float target_area = extrusion_area_for_width_height_for_gcode(target_width_mm, height_mm); + if (!std::isfinite(base_area) || base_area <= 0.f || !std::isfinite(target_area)) + return 1.0; + + return std::clamp(double(target_area / base_area), 0.01, 10.0); +} + +static double bbox_distance_sq_to_point_for_gcode(const BoundingBox &bbox, const Point &point) +{ + if (!bbox.defined) + return 0.0; + + double dx = 0.0; + if (point.x() < bbox.min.x()) + dx = double(bbox.min.x() - point.x()); + else if (point.x() > bbox.max.x()) + dx = double(point.x() - bbox.max.x()); + + double dy = 0.0; + if (point.y() < bbox.min.y()) + dy = double(bbox.min.y() - point.y()); + else if (point.y() > bbox.max.y()) + dy = double(point.y() - bbox.max.y()); + + return dx * dx + dy * dy; +} + +static bool find_nearest_layer_slice_boundary_point_for_gcode(const Layer *layer, const Point &query_point, Point &nearest_point) +{ + if (layer == nullptr || layer->lslices.empty()) + return false; + + const bool has_slice_bboxes = layer->lslices_bboxes.size() == layer->lslices.size(); + double best_distance_sq = std::numeric_limits::max(); + bool found = false; + + for (size_t slice_idx = 0; slice_idx < layer->lslices.size(); ++slice_idx) { + const ExPolygon &slice = layer->lslices[slice_idx]; + if (slice.empty()) + continue; + + if (has_slice_bboxes && layer->lslices_bboxes[slice_idx].defined) { + const double bbox_distance_sq = bbox_distance_sq_to_point_for_gcode(layer->lslices_bboxes[slice_idx], query_point); + if (bbox_distance_sq > best_distance_sq) + continue; + } + + const Point projected = slice.point_projection(query_point); + const double projected_distance_sq = (projected - query_point).cast().squaredNorm(); + if (projected_distance_sq < best_distance_sq) { + best_distance_sq = projected_distance_sq; + nearest_point = projected; + found = true; + } + } + + return found; +} + +static void choose_segment_outward_normal_from_reference_for_gcode(double reference_x, + double reference_y, + double n0x, + double n0y, + double n1x, + double n1y, + double &outward_x, + double &outward_y) +{ + const double dot0 = n0x * reference_x + n0y * reference_y; + const double dot1 = n1x * reference_x + n1y * reference_y; + if (dot1 > dot0) { + outward_x = n1x; + outward_y = n1y; + } else { + outward_x = n0x; + outward_y = n0y; + } +} + +static void resolve_segment_shift_outward_normal_for_gcode(const Layer *layer, + const Point &mid_point, + double dx, + double dy, + double len, + double fallback_reference_x, + double fallback_reference_y, + double &outward_x, + double &outward_y) +{ + const double n0x = dy / len; + const double n0y = -dx / len; + const double n1x = -n0x; + const double n1y = -n0y; + + Point nearest_boundary_point; + if (find_nearest_layer_slice_boundary_point_for_gcode(layer, mid_point, nearest_boundary_point)) { + const double boundary_x = double(nearest_boundary_point.x()) - double(mid_point.x()); + const double boundary_y = double(nearest_boundary_point.y()) - double(mid_point.y()); + const double boundary_len2 = boundary_x * boundary_x + boundary_y * boundary_y; + if (boundary_len2 > 1e-6) { + const double boundary_len = std::sqrt(boundary_len2); + const double normal_alignment = std::abs(n0x * boundary_x + n0y * boundary_y) / std::max(boundary_len, 1e-9); + if (normal_alignment >= 0.25) { + choose_segment_outward_normal_from_reference_for_gcode(boundary_x, boundary_y, n0x, n0y, n1x, n1y, outward_x, outward_y); + return; + } + } + } + + const double fallback_len2 = fallback_reference_x * fallback_reference_x + fallback_reference_y * fallback_reference_y; + if (fallback_len2 > 1e-6) { + choose_segment_outward_normal_from_reference_for_gcode(fallback_reference_x, fallback_reference_y, n0x, n0y, n1x, n1y, outward_x, outward_y); + return; + } + + outward_x = n0x; + outward_y = n0y; +} + +static bool clamped_shift_coord_for_gcode(double direction_component, double shift_scaled, coord_t max_abs_shift, coord_t &out) +{ + if (!std::isfinite(direction_component) || !std::isfinite(shift_scaled)) + return false; + + const double raw = direction_component * shift_scaled; + if (!std::isfinite(raw)) + return false; + + const double max_shift = std::max(0.0, double(max_abs_shift)); + double clamped = std::clamp(raw, -max_shift, max_shift); + clamped = std::clamp(clamped, double(std::numeric_limits::lowest()), double(std::numeric_limits::max())); + + out = coord_t(std::llround(clamped)); + return true; +} + +static bool is_reasonable_quantized_gcode_point_for_gcode(const Vec2d &p) +{ + constexpr double k_abs_coord_limit_mm = 10000.0; + return std::isfinite(p(0)) && std::isfinite(p(1)) && + std::abs(p(0)) <= k_abs_coord_limit_mm && + std::abs(p(1)) <= k_abs_coord_limit_mm; +} + +static float repeated_rotation_progress_for_gcode(float progress01, float repeats, bool reverse_repeats) +{ + const float p = clamp01f_for_gcode(progress01); + const float r = std::max(1.f, repeats); + if (r <= 1.f + EPSILON) + return p; + + float repeated_pos = p * r; + int segment_idx = int(std::floor(repeated_pos)); + float local = repeated_pos - float(segment_idx); + + if (p >= 1.f - EPSILON) { + segment_idx = std::max(0, int(std::ceil(r)) - 1); + local = 1.f; + } + + if (reverse_repeats && (segment_idx % 2 == 1)) + local = 1.f - local; + return clamp01f_for_gcode(local); +} + +static float offset_fade_factor_for_gcode(int fade_mode, float progress01) +{ + const float p = clamp01f_for_gcode(progress01); + switch (fade_mode) { + case int(TextureMappingZone::OffsetFadeInUp): + return p; + case int(TextureMappingZone::OffsetFadeOutUp): + return 1.f - p; + case int(TextureMappingZone::OffsetFadeInOut): + return 1.f - std::abs(2.f * p - 1.f); + case int(TextureMappingZone::OffsetFadeOutIn): + return std::abs(2.f * p - 1.f); + case int(TextureMappingZone::OffsetFadeOutInReversed): + return 2.f * p - 1.f; + default: + return 1.f; + } +} + +static bool has_explicit_offset_gradient_profile_for_gcode(const TextureMappingZone &zone) +{ + return zone.has_custom_offset_settings(); +} + +static float overhang_filament_strength_factor_for_gcode(const TextureMappingZone &zone, unsigned int physical_filament_id) +{ + if (physical_filament_id == 0) + return 1.f; + + const size_t idx = size_t(physical_filament_id - 1); + if (idx >= zone.filament_strengths_pct.size()) + return 1.f; + + const float strength_pct = zone.filament_strengths_pct[idx]; + if (!std::isfinite(strength_pct)) + return 1.f; + + return std::clamp(strength_pct / 100.f, 0.f, 1.f); +} + +static float overhang_filament_minimum_offset_factor_for_gcode(const TextureMappingZone &zone, unsigned int physical_filament_id) +{ + if (physical_filament_id == 0) + return 0.f; + + const size_t idx = size_t(physical_filament_id - 1); + if (idx >= zone.filament_minimum_offsets_pct.size()) + return 0.f; + + const float minimum_offset_pct = zone.filament_minimum_offsets_pct[idx]; + if (!std::isfinite(minimum_offset_pct)) + return 0.f; + + return std::clamp(minimum_offset_pct / 100.f, 0.f, 1.f); +} + +static float variable_width_delta_for_overhang_range_for_gcode(float inset_strength, + float max_width_delta_limit_mm, + float minimum_offset_factor, + float strength_factor) +{ + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= 0.f) + return 0.f; + + const float desired_width_factor = 1.f - std::clamp(inset_strength, 0.f, 1.f); + const float min_width_factor = std::clamp(minimum_offset_factor, 0.f, 1.f); + const float adjusted_width_factor = + min_width_factor + desired_width_factor * std::clamp(strength_factor, 0.f, 1.f) * (1.f - min_width_factor); + + return std::clamp(max_width_delta_limit_mm * (1.f - adjusted_width_factor), 0.f, max_width_delta_limit_mm); +} + +static float nonlinear_visibility_width_factor_for_gcode(float desired_width_factor, + float layer_height_mm, + float stair_step_mm, + float max_width_delta_limit_mm, + float sagging_ratio) +{ + const float r = clamp01f_for_gcode(desired_width_factor); + if (!std::isfinite(layer_height_mm) || + !std::isfinite(stair_step_mm) || + !std::isfinite(max_width_delta_limit_mm) || + layer_height_mm <= EPSILON || + max_width_delta_limit_mm <= EPSILON) + return r; + + if (r <= EPSILON || r >= 1.f - EPSILON) + return r; + if (std::abs(r - 0.5f) <= 1e-5f) + return 0.5f; + + const float h = std::max(0.01f, layer_height_mm); + const float d = std::max(0.f, stair_step_mm); + const float diag = std::hypot(h, d); + if (!std::isfinite(diag) || diag <= EPSILON) + return r; + + const float symmetric_r = std::min(r, 1.f - r); + const float direction = r >= 0.5f ? 1.f : -1.f; + const float sin_n = std::clamp(d / diag, 0.f, 1.f); + const float cos_n = std::clamp(h / diag, 1e-4f, 1.f); + const float sin_cos = sin_n * cos_n; + float offset_mm = 0.f; + + if (sin_cos > 1e-5f) { + offset_mm = (0.5f - symmetric_r) * h / sin_cos; + if (2.f * std::abs(offset_mm) <= d + EPSILON) + return std::clamp(0.5f + direction * offset_mm / max_width_delta_limit_mm, 0.f, 1.f); + } + + const float effective_sagging_ratio = + std::max(2.f, std::isfinite(sagging_ratio) && sagging_ratio > EPSILON ? sagging_ratio : 2.f); + const float cx = std::clamp(1.f - std::sqrt(2.f) / effective_sagging_ratio, 0.f, 0.95f); + const float c = (1.f - cx) * (1.f - cx); + const float safe_cos = std::max(cos_n, 1e-4f); + const float tan_n = sin_n / safe_cos; + const float a = -0.5f * c * (1.f + sin_n) / std::max(h * diag, 1e-6f); + const float b = 0.5f * (c * tan_n * (1.f + sin_n) + 2.f * cos_n * (cx - 1.f)) / std::max(diag, 1e-6f); + const float q = c * 0.25f * tan_n * (1.f + sin_n) - cx * cos_n; + const float cc = 0.5f - 0.5f * cos_n * q - symmetric_r; + const float det = std::max(0.f, b * b - 4.f * a * cc); + if (std::abs(a) > 1e-8f) { + offset_mm = (-b - std::sqrt(det)) / (2.f * a); + if (!std::isfinite(offset_mm) || offset_mm < 0.f) + offset_mm = (-b + std::sqrt(det)) / (2.f * a); + } + if (!std::isfinite(offset_mm) || offset_mm < 0.f) { + if (sin_cos > 1e-5f) + offset_mm = (0.5f - symmetric_r) * h / sin_cos; + else + offset_mm = max_width_delta_limit_mm; + } + + return std::clamp(0.5f + direction * offset_mm / max_width_delta_limit_mm, 0.f, 1.f); +} + +static float variable_width_delta_for_visibility_range_for_gcode(float inset_strength, + float max_width_delta_limit_mm, + float minimum_offset_factor, + float strength_factor, + bool nonlinear_offset_adjustment, + float layer_height_mm, + float stair_step_mm, + float sagging_ratio) +{ + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= 0.f) + return 0.f; + + float desired_width_factor = 1.f - std::clamp(inset_strength, 0.f, 1.f); + if (nonlinear_offset_adjustment) + desired_width_factor = nonlinear_visibility_width_factor_for_gcode(desired_width_factor, + layer_height_mm, + stair_step_mm, + max_width_delta_limit_mm, + sagging_ratio); + + const float min_width_factor = std::clamp(minimum_offset_factor, 0.f, 1.f); + const float adjusted_width_factor = + min_width_factor + desired_width_factor * std::clamp(strength_factor, 0.f, 1.f) * (1.f - min_width_factor); + + return std::clamp(max_width_delta_limit_mm * (1.f - adjusted_width_factor), 0.f, max_width_delta_limit_mm); +} + +static float local_surface_stair_step_distance_for_gcode(const Layer *layer, + const Point &mid_point, + double outward_x, + double outward_y, + float base_outer_width_mm, + float max_allowed_distance_mm) +{ + if (layer == nullptr || !std::isfinite(outward_x) || !std::isfinite(outward_y)) + return std::numeric_limits::quiet_NaN(); + + const double half_width_scaled = scale_(0.5 * double(std::max(0.01f, base_outer_width_mm))); + const Point current_base_edge( + coord_t(std::llround(double(mid_point.x()) + outward_x * half_width_scaled)), + coord_t(std::llround(double(mid_point.y()) + outward_y * half_width_scaled))); + const float max_local_edge_tangent_delta_mm = std::max(1.0f, base_outer_width_mm * 2.f); + const float max_local_edge_normal_delta_mm = + std::max(2.0f, base_outer_width_mm * 4.f + 2.f * std::max(0.f, max_allowed_distance_mm)); + float best_distance_mm = std::numeric_limits::quiet_NaN(); + + auto consider_adjacent_layer = [&](const Layer *adjacent_layer) { + if (adjacent_layer == nullptr) + return; + + Point adjacent_base_edge; + if (!find_nearest_layer_slice_boundary_point_for_gcode(adjacent_layer, current_base_edge, adjacent_base_edge)) + return; + + const double edge_delta_x = double(adjacent_base_edge.x()) - double(current_base_edge.x()); + const double edge_delta_y = double(adjacent_base_edge.y()) - double(current_base_edge.y()); + const double edge_distance_scaled = std::hypot(edge_delta_x, edge_delta_y); + const double edge_normal_delta_scaled = edge_delta_x * outward_x + edge_delta_y * outward_y; + const double edge_tangent_delta_scaled_sq = + std::max(0.0, edge_distance_scaled * edge_distance_scaled - edge_normal_delta_scaled * edge_normal_delta_scaled); + const float edge_tangent_delta_mm = unscale(std::sqrt(edge_tangent_delta_scaled_sq)); + if (!std::isfinite(edge_tangent_delta_mm) || edge_tangent_delta_mm > max_local_edge_tangent_delta_mm) + return; + + const float edge_normal_delta_mm = std::abs(unscale(edge_normal_delta_scaled)); + if (!std::isfinite(edge_normal_delta_mm) || edge_normal_delta_mm > max_local_edge_normal_delta_mm) + return; + + if (!std::isfinite(best_distance_mm) || edge_normal_delta_mm < best_distance_mm) + best_distance_mm = edge_normal_delta_mm; + }; + + consider_adjacent_layer(layer->upper_layer); + consider_adjacent_layer(layer->lower_layer); + return best_distance_mm; +} + +static bool is_horizontal_overhang_gradient_row_for_gcode(const TextureMappingZone &zone) +{ + return zone.enabled && !zone.deleted && (zone.is_2d_gradient() || zone.is_image_texture()); +} + +static bool is_vertex_color_match_overhang_row_for_gcode(const TextureMappingZone &zone) +{ + return zone.enabled && !zone.deleted && zone.is_image_texture(); +} + +static bool is_2d_offset_gradient_row_for_gcode(const TextureMappingZone &zone) +{ + return zone.enabled && !zone.deleted && zone.is_2d_gradient(); +} + +static std::array unpack_rgba_u32(uint32_t packed_rgba) +{ + const float r = float((packed_rgba >> 24) & 0xFFu) / 255.f; + const float g = float((packed_rgba >> 16) & 0xFFu) / 255.f; + const float b = float((packed_rgba >> 8) & 0xFFu) / 255.f; + const float a = float(packed_rgba & 0xFFu) / 255.f; + return { clamp01f_for_gcode(r), clamp01f_for_gcode(g), clamp01f_for_gcode(b), clamp01f_for_gcode(a) }; +} + +static std::array mix_component_colors_with_filament_mixer_for_gcode(const std::vector> &component_colors, + const std::vector &weights) +{ + if (component_colors.empty() || component_colors.size() != weights.size()) + return { 0.f, 0.f, 0.f }; + + bool has_base = false; + float out_r = 0.f; + float out_g = 0.f; + float out_b = 0.f; + int accumulated = 0; + for (size_t i = 0; i < component_colors.size(); ++i) { + const int weight = std::max(0, weights[i]); + if (weight == 0) + continue; + + if (!has_base) { + out_r = component_colors[i][0]; + out_g = component_colors[i][1]; + out_b = component_colors[i][2]; + accumulated = weight; + has_base = true; + continue; + } + + const float t = float(weight) / float(std::max(1, accumulated + weight)); + float mixed_r = out_r; + float mixed_g = out_g; + float mixed_b = out_b; + filament_mixer_lerp_float(out_r, out_g, out_b, + component_colors[i][0], component_colors[i][1], component_colors[i][2], + t, + &mixed_r, &mixed_g, &mixed_b); + out_r = clamp01f_for_gcode(mixed_r); + out_g = clamp01f_for_gcode(mixed_g); + out_b = clamp01f_for_gcode(mixed_b); + accumulated += weight; + } + + if (!has_base) + return component_colors.front(); + return { out_r, out_g, out_b }; +} + +static std::vector best_component_mix_weights_for_target_for_gcode(const std::vector> &component_colors, + const std::array &target_rgb) +{ + if (component_colors.empty()) + return {}; + if (component_colors.size() == 1) + return { 1.f }; + + const size_t component_count = component_colors.size(); + const int total_units = component_count <= 4 ? 20 : (component_count <= 6 ? 10 : 6); + std::vector units(component_count, 0); + std::vector best_units(component_count, 0); + float best_error = std::numeric_limits::max(); + + std::function recurse = [&](size_t idx, int remaining_units) { + if (idx + 1 == component_count) { + units[idx] = remaining_units; + const std::array mixed = mix_component_colors_with_filament_mixer_for_gcode(component_colors, units); + const float dr = mixed[0] - target_rgb[0]; + const float dg = mixed[1] - target_rgb[1]; + const float db = mixed[2] - target_rgb[2]; + const float error = dr * dr + dg * dg + db * db; + if (error < best_error) { + best_error = error; + best_units = units; + } + return; + } + + for (int u = 0; u <= remaining_units; ++u) { + units[idx] = u; + recurse(idx + 1, remaining_units - u); + } + }; + recurse(0, total_units); + + std::vector weights(component_count, 0.f); + for (size_t i = 0; i < component_count; ++i) + weights[i] = float(best_units[i]) / float(std::max(1, total_units)); + return weights; +} + +static float apply_texture_tone_gamma_for_gcode(float channel, float tone_gamma) +{ + const float safe_channel = clamp01f_for_gcode(channel); + const float safe_gamma = + (!std::isfinite(tone_gamma) || tone_gamma <= 0.f) ? 1.f : std::clamp(tone_gamma, 0.5f, 3.f); + if (std::abs(safe_gamma - 1.f) <= 1e-5f) + return safe_channel; + return clamp01f_for_gcode(std::pow(safe_channel, 1.f / safe_gamma)); +} + +static void apply_texture_contrast_to_mapped_components_for_gcode(std::vector &component_weights, + float contrast_factor, + size_t mapped_component_count) +{ + const size_t count = std::min(mapped_component_count, component_weights.size()); + if (count == 0) + return; + + float mean_weight = 0.f; + for (size_t idx = 0; idx < count; ++idx) + mean_weight += clamp01f_for_gcode(component_weights[idx]); + mean_weight /= float(count); + + for (size_t idx = 0; idx < count; ++idx) { + const float safe_weight = clamp01f_for_gcode(component_weights[idx]); + component_weights[idx] = clamp01f_for_gcode(mean_weight + (safe_weight - mean_weight) * contrast_factor); + } +} + +static float wrap_repeat01_for_gcode(float uv) +{ + if (!std::isfinite(uv)) + return 0.f; + + constexpr float k_uv_epsilon = 1e-6f; + if (uv >= -k_uv_epsilon && uv <= 1.f + k_uv_epsilon) + return std::clamp(uv, 0.f, 1.f); + + float wrapped = uv - std::floor(uv); + if (wrapped < 0.f) + wrapped += 1.f; + return wrapped; +} + +static std::array sample_texture_rgba_bilinear_for_gcode(const std::vector &rgba, + uint32_t width, + uint32_t height, + float u, + float v) +{ + if (width == 0 || height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return { 0.f, 0.f, 0.f, 1.f }; + + const float uu = wrap_repeat01_for_gcode(u); + const float vv = wrap_repeat01_for_gcode(v); + + const float x = uu * float(width > 1 ? width - 1 : 0); + const float y = vv * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&rgba, width](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * 4 + channel; + return float(rgba[idx]) / 255.f; + }; + + std::array out{}; + for (size_t c = 0; c < 4; ++c) { + const float c00 = sample_channel(x0, y0, c); + const float c10 = sample_channel(x1, y0, c); + const float c01 = sample_channel(x0, y1, c); + const float c11 = sample_channel(x1, y1, c); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + out[c] = clamp01f_for_gcode(cx0 + (cx1 - cx0) * ty); + } + return out; +} + +static std::array unwrap_triangle_uvs_for_sampling_for_gcode(const Vec2f &uv0, + const Vec2f &uv1, + const Vec2f &uv2) +{ + std::array out { uv0, uv1, uv2 }; + + auto unwrap_axis = [&out](bool use_u_axis) { + float values[3] = { + use_u_axis ? out[0].x() : out[0].y(), + use_u_axis ? out[1].x() : out[1].y(), + use_u_axis ? out[2].x() : out[2].y() + }; + const float v_min = std::min({ values[0], values[1], values[2] }); + const float v_max = std::max({ values[0], values[1], values[2] }); + if (v_max - v_min <= 0.5f) + return; + + for (size_t i = 0; i < 3; ++i) { + if (values[i] < 0.5f) + values[i] += 1.f; + } + + if (use_u_axis) { + out[0].x() = values[0]; + out[1].x() = values[1]; + out[2].x() = values[2]; + } else { + out[0].y() = values[0]; + out[1].y() = values[1]; + out[2].y() = values[2]; + } + }; + + unwrap_axis(true); + unwrap_axis(false); + return out; +} + +static float color_distance_sq_for_gcode(const std::array &lhs, const std::array &rhs) +{ + const float dr = lhs[0] - rhs[0]; + const float dg = lhs[1] - rhs[1]; + const float db = lhs[2] - rhs[2]; + return dr * dr + dg * dg + db * db; +} + +static std::vector best_matching_component_indices_for_semantic_colors_for_gcode( + const std::vector> &component_colors, + const std::vector> &semantic_colors) +{ + if (component_colors.empty() || component_colors.size() != semantic_colors.size()) + return {}; + + std::vector permutation(component_colors.size(), 0); + std::iota(permutation.begin(), permutation.end(), size_t(0)); + + std::vector best_permutation = permutation; + float best_error = std::numeric_limits::max(); + do { + float error = 0.f; + for (size_t role_idx = 0; role_idx < semantic_colors.size(); ++role_idx) + error += color_distance_sq_for_gcode(component_colors[permutation[role_idx]], semantic_colors[role_idx]); + + if (error < best_error) { + best_error = error; + best_permutation = permutation; + } + } while (std::next_permutation(permutation.begin(), permutation.end())); + + return best_permutation; +} + +static std::vector semantic_component_indices_for_gcode(const std::vector> &component_colors, + int filament_color_mode, + bool force_sequential_filaments) +{ + if (force_sequential_filaments) + return {}; + + std::vector> semantic_colors; + switch (filament_color_mode) { + case int(TextureMappingZone::FilamentColorRGB): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMY): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYK): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 0.f, 0.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYW): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorRGBK): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 0.f, 0.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorRGBW): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 1.f, 1.f, 1.f } } }; + break; + default: + return {}; + } + + return best_matching_component_indices_for_semantic_colors_for_gcode(component_colors, semantic_colors); +} + +static std::vector optimized_primary_component_weights_for_target_for_gcode(const std::array &target_rgb, + size_t component_count, + int filament_color_mode, + const std::vector> &component_colors, + bool force_sequential_filaments) +{ + const int clamped_mode = std::clamp(filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + if (clamped_mode == int(TextureMappingZone::FilamentColorAny)) + return {}; + + auto print_visibility_strength = [](float value) { + return clamp01f_for_gcode(std::pow(std::max(0.f, value), 0.85f)); + }; + + const float r = clamp01f_for_gcode(target_rgb[0]); + const float g = clamp01f_for_gcode(target_rgb[1]); + const float b = clamp01f_for_gcode(target_rgb[2]); + const float whiteness = std::min({ r, g, b }); + const float darkness = 1.f - std::max({ r, g, b }); + + auto safe_div = [](float numerator, float denominator) { + if (denominator <= EPSILON) + return 0.f; + return clamp01f_for_gcode(numerator / denominator); + }; + + const std::vector semantic_component_indices = + semantic_component_indices_for_gcode(component_colors, clamped_mode, force_sequential_filaments); + const auto component_index_for_role = [&semantic_component_indices](size_t role_idx) { + if (role_idx < semantic_component_indices.size()) + return semantic_component_indices[role_idx]; + return role_idx; + }; + + std::vector weights(component_count, 0.f); + if (clamped_mode == int(TextureMappingZone::FilamentColorRGB)) { + if (component_count != 3) + return {}; + weights[component_index_for_role(0)] = print_visibility_strength(target_rgb[0]); + weights[component_index_for_role(1)] = print_visibility_strength(target_rgb[1]); + weights[component_index_for_role(2)] = print_visibility_strength(target_rgb[2]); + return weights; + } + + if (clamped_mode == int(TextureMappingZone::FilamentColorCMY)) { + if (component_count != 3) + return {}; + weights[component_index_for_role(0)] = print_visibility_strength(1.f - r); + weights[component_index_for_role(1)] = print_visibility_strength(1.f - g); + weights[component_index_for_role(2)] = print_visibility_strength(1.f - b); + return weights; + } + + if (clamped_mode == int(TextureMappingZone::FilamentColorBW)) { + if (component_count != 2) + return {}; + + const float gray = clamp01f_for_gcode(0.2126f * r + 0.7152f * g + 0.0722f * b); + const float black_strength = gray >= 0.5f ? (2.f * (1.f - gray)) : 1.f; + const float white_strength = gray <= 0.5f ? (2.f * gray) : 1.f; + size_t black_component_idx = 0; + size_t white_component_idx = 1; + if (!force_sequential_filaments && component_colors.size() >= 2) { + const float lum0 = 0.2126f * component_colors[0][0] + 0.7152f * component_colors[0][1] + 0.0722f * component_colors[0][2]; + const float lum1 = 0.2126f * component_colors[1][0] + 0.7152f * component_colors[1][1] + 0.0722f * component_colors[1][2]; + if (lum0 > lum1) { + black_component_idx = 1; + white_component_idx = 0; + } + } + + weights[black_component_idx] = print_visibility_strength(black_strength); + weights[white_component_idx] = print_visibility_strength(white_strength); + return weights; + } + + if (component_count != 4) + return {}; + + if (clamped_mode == int(TextureMappingZone::FilamentColorCMYK)) { + const float k = clamp01f_for_gcode(darkness); + const float inv = 1.f - k; + weights[component_index_for_role(0)] = print_visibility_strength(safe_div(1.f - r - k, inv)); + weights[component_index_for_role(1)] = print_visibility_strength(safe_div(1.f - g - k, inv)); + weights[component_index_for_role(2)] = print_visibility_strength(safe_div(1.f - b - k, inv)); + weights[component_index_for_role(3)] = print_visibility_strength(k); + return weights; + } + + if (clamped_mode == int(TextureMappingZone::FilamentColorCMYW)) { + const float inv = 1.f - whiteness; + const float r_no_w = safe_div(r - whiteness, inv); + const float g_no_w = safe_div(g - whiteness, inv); + const float b_no_w = safe_div(b - whiteness, inv); + weights[component_index_for_role(0)] = print_visibility_strength(clamp01f_for_gcode((1.f - r_no_w) * inv)); + weights[component_index_for_role(1)] = print_visibility_strength(clamp01f_for_gcode((1.f - g_no_w) * inv)); + weights[component_index_for_role(2)] = print_visibility_strength(clamp01f_for_gcode((1.f - b_no_w) * inv)); + weights[component_index_for_role(3)] = clamp01f_for_gcode(std::pow(whiteness, 1.35f)); + return weights; + } + + if (clamped_mode == int(TextureMappingZone::FilamentColorRGBK)) { + const float k = clamp01f_for_gcode(darkness); + const float inv = 1.f - k; + weights[component_index_for_role(0)] = print_visibility_strength(safe_div(r - k, inv)); + weights[component_index_for_role(1)] = print_visibility_strength(safe_div(g - k, inv)); + weights[component_index_for_role(2)] = print_visibility_strength(safe_div(b - k, inv)); + weights[component_index_for_role(3)] = print_visibility_strength(k); + return weights; + } + + if (clamped_mode == int(TextureMappingZone::FilamentColorRGBW)) { + const float inv = 1.f - whiteness; + weights[component_index_for_role(0)] = print_visibility_strength(safe_div(r - whiteness, inv)); + weights[component_index_for_role(1)] = print_visibility_strength(safe_div(g - whiteness, inv)); + weights[component_index_for_role(2)] = print_visibility_strength(safe_div(b - whiteness, inv)); + weights[component_index_for_role(3)] = print_visibility_strength(whiteness); + return weights; + } + + return {}; +} + +static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode(const PrintObject &print_object, + const std::vector> &component_colors, + bool raw_values_mode, + int filament_color_mode, + bool force_sequential_filaments, + float texture_contrast_pct, + float texture_tone_gamma, + bool layer_aware_weighting, + float layer_z_mm, + float layer_z_falloff_mm, + bool high_resolution_texture_sampling) +{ + VertexColorOverhangWeightField weight_field; + if (component_colors.empty()) + return weight_field; + + const ModelObject *model_object = print_object.model_object(); + if (model_object == nullptr) + return weight_field; + + const BoundingBox object_bbox = print_object.bounding_box(); + const float min_x_mm = unscale(object_bbox.min.x()); + const float min_y_mm = unscale(object_bbox.min.y()); + const float max_x_mm = unscale(object_bbox.max.x()); + const float max_y_mm = unscale(object_bbox.max.y()); + const float span_x_mm = std::max(max_x_mm - min_x_mm, 1e-3f); + const float span_y_mm = std::max(max_y_mm - min_y_mm, 1e-3f); + if (!std::isfinite(min_x_mm) || !std::isfinite(min_y_mm) || + !std::isfinite(max_x_mm) || !std::isfinite(max_y_mm) || + !std::isfinite(span_x_mm) || !std::isfinite(span_y_mm)) + return VertexColorOverhangWeightField{}; + + const bool use_layer_weighting = layer_aware_weighting && std::isfinite(layer_z_mm); + const float safe_layer_z_falloff_mm = std::max(layer_z_falloff_mm, 1e-3f); + const float contrast_factor = std::clamp(texture_contrast_pct, 25.f, 300.f) / 100.f; + const float tone_gamma = + (!std::isfinite(texture_tone_gamma) || texture_tone_gamma <= 0.f) ? 1.f : std::clamp(texture_tone_gamma, 0.5f, 3.f); + + struct WeightedTextureSample { + float x_mm { 0.f }; + float y_mm { 0.f }; + std::array rgba { { 0.f, 0.f, 0.f, 1.f } }; + float weight { 0.f }; + }; + std::vector samples; + samples.reserve(8192); + + auto accumulate_sample = [&samples](float x_mm, float y_mm, const std::array &rgba, float sample_weight) { + if (!std::isfinite(x_mm) || !std::isfinite(y_mm) || sample_weight <= EPSILON) + return; + if (!std::isfinite(sample_weight) || + !std::isfinite(rgba[0]) || + !std::isfinite(rgba[1]) || + !std::isfinite(rgba[2]) || + !std::isfinite(rgba[3])) + return; + + samples.push_back({ x_mm, y_mm, rgba, sample_weight }); + }; + + const Transform3d object_trafo = print_object.trafo_centered(); + for (const ModelVolume *volume : model_object->volumes) { + if (volume == nullptr) + continue; + + const std::shared_ptr mesh_ptr = volume->mesh_ptr(); + if (!mesh_ptr) + continue; + + const indexed_triangle_set &its = mesh_ptr->its; + const Transform3d volume_trafo = object_trafo * volume->get_matrix(); + + bool sampled_from_uv_texture = false; + const bool has_uv_texture = + !volume->imported_texture_rgba.empty() && + volume->imported_texture_width > 0 && + volume->imported_texture_height > 0 && + volume->imported_texture_uv_valid.size() == its.indices.size() && + volume->imported_texture_uvs_per_face.size() >= its.indices.size() * 6 && + volume->imported_texture_rgba.size() >= size_t(volume->imported_texture_width) * size_t(volume->imported_texture_height) * 4; + + if (has_uv_texture) { + const auto uv_edge_texel_length = [volume](const Vec2f &a, const Vec2f &b) { + const float du = (a.x() - b.x()) * float(volume->imported_texture_width); + const float dv = (a.y() - b.y()) * float(volume->imported_texture_height); + return std::hypot(du, dv); + }; + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + if (volume->imported_texture_uv_valid[tri_idx] == 0) + continue; + + const auto &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + continue; + if (size_t(tri[0]) >= its.vertices.size() || + size_t(tri[1]) >= its.vertices.size() || + size_t(tri[2]) >= its.vertices.size()) + continue; + + const Vec3d p0 = volume_trafo * its.vertices[size_t(tri[0])].cast(); + const Vec3d p1 = volume_trafo * its.vertices[size_t(tri[1])].cast(); + const Vec3d p2 = volume_trafo * its.vertices[size_t(tri[2])].cast(); + if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) + continue; + + const size_t uv_off = tri_idx * 6; + const Vec2f uv0(volume->imported_texture_uvs_per_face[uv_off + 0], volume->imported_texture_uvs_per_face[uv_off + 1]); + const Vec2f uv1(volume->imported_texture_uvs_per_face[uv_off + 2], volume->imported_texture_uvs_per_face[uv_off + 3]); + const Vec2f uv2(volume->imported_texture_uvs_per_face[uv_off + 4], volume->imported_texture_uvs_per_face[uv_off + 5]); + if (!uv0.allFinite() || !uv1.allFinite() || !uv2.allFinite()) + continue; + const std::array tri_uv = unwrap_triangle_uvs_for_sampling_for_gcode(uv0, uv1, uv2); + + const float max_uv_edge_texel = std::max({ + uv_edge_texel_length(tri_uv[0], tri_uv[1]), + uv_edge_texel_length(tri_uv[1], tri_uv[2]), + uv_edge_texel_length(tri_uv[2], tri_uv[0]) + }); + const float max_world_edge_mm = std::max({ + float((p1 - p0).norm()), + float((p2 - p1).norm()), + float((p0 - p2).norm()) + }); + if (!std::isfinite(max_uv_edge_texel) || !std::isfinite(max_world_edge_mm)) + continue; + + 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 int uv_steps = std::clamp(int(std::ceil(max_uv_edge_texel / uv_texels_per_step)), 1, max_bary_steps); + const int world_steps = std::clamp(int(std::ceil(max_world_edge_mm / world_sample_pitch_mm)), 1, max_bary_steps); + const int bary_steps = std::max(uv_steps, world_steps); + const int sample_count = bary_steps * (bary_steps + 1) / 2; + if (sample_count <= 0) + continue; + + const double tri_area_mm2 = 0.5 * ((p1 - p0).cross(p2 - p0)).norm(); + if (!std::isfinite(tri_area_mm2)) + continue; + const float area_weight = std::max(0.05f, float(tri_area_mm2)) / float(sample_count); + if (!std::isfinite(area_weight)) + continue; + const float inv_steps = 1.f / float(bary_steps); + + for (int i = 0; i < bary_steps; ++i) { + for (int j = 0; j < (bary_steps - i); ++j) { + const float b1 = (float(i) + 0.33333334f) * inv_steps; + const float b2 = (float(j) + 0.33333334f) * inv_steps; + const float b0 = 1.f - b1 - b2; + if (b0 < 0.f) + continue; + + const Vec3d world_pos = p0 * double(b0) + p1 * double(b1) + p2 * double(b2); + const Vec2f uv = tri_uv[0] * b0 + tri_uv[1] * b1 + tri_uv[2] * b2; + std::array rgba = sample_texture_rgba_bilinear_for_gcode(volume->imported_texture_rgba, + volume->imported_texture_width, + volume->imported_texture_height, + uv.x(), + uv.y()); + rgba[3] = 1.f; + + float sample_weight = area_weight; + if (use_layer_weighting) { + const float dz = std::abs(float(world_pos.z()) - layer_z_mm); + const float z_norm = dz / safe_layer_z_falloff_mm; + const float z_weight = std::exp(-0.5f * z_norm * z_norm); + if (!std::isfinite(z_weight)) + continue; + sample_weight *= z_weight; + } + if (sample_weight <= EPSILON) + continue; + + accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, sample_weight); + sampled_from_uv_texture = true; + } + } + } + } + + if (sampled_from_uv_texture) + continue; + + if (volume->imported_vertex_colors_rgba.empty()) + continue; + if (its.vertices.size() != volume->imported_vertex_colors_rgba.size()) + continue; + + for (size_t i = 0; i < its.vertices.size(); ++i) { + const Vec3d world_pos = volume_trafo * its.vertices[i].cast(); + std::array rgba = unpack_rgba_u32(volume->imported_vertex_colors_rgba[i]); + rgba[3] = 1.f; + float sample_weight = 1.f; + if (use_layer_weighting) { + const float dz = std::abs(float(world_pos.z()) - layer_z_mm); + const float z_norm = dz / safe_layer_z_falloff_mm; + const float z_weight = std::exp(-0.5f * z_norm * z_norm); + if (!std::isfinite(z_weight)) + continue; + sample_weight *= z_weight; + } + if (sample_weight <= EPSILON) + continue; + + accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, sample_weight); + } + } + + if (samples.empty()) + return VertexColorOverhangWeightField{}; + + const size_t component_count = component_colors.size(); + const size_t sample_count = samples.size(); + + weight_field.component_count = component_count; + weight_field.sample_x_mm.resize(sample_count); + weight_field.sample_y_mm.resize(sample_count); + weight_field.sample_weight.resize(sample_count); + weight_field.sample_component_weights.assign(sample_count * component_count, 0.f); + + std::vector fallback_acc(component_count, 0.f); + float fallback_weight = 0.f; + for (size_t sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const WeightedTextureSample &sample = samples[sample_idx]; + if (sample.weight <= EPSILON) + continue; + + weight_field.sample_x_mm[sample_idx] = sample.x_mm; + weight_field.sample_y_mm[sample_idx] = sample.y_mm; + weight_field.sample_weight[sample_idx] = sample.weight; + + std::array 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); + } + + std::vector desired(component_count, 0.f); + size_t mapped_component_count = component_count; + if (raw_values_mode) { + const float channels[3] = { target[0], target[1], target[2] }; + const size_t channel_count = std::min(component_count, size_t(3)); + for (size_t channel_idx = 0; channel_idx < channel_count; ++channel_idx) + desired[channel_idx] = clamp01f_for_gcode(channels[channel_idx]); + mapped_component_count = channel_count; + } else { + std::vector optimized = optimized_primary_component_weights_for_target_for_gcode(target, + component_count, + filament_color_mode, + component_colors, + force_sequential_filaments); + if (optimized.size() == component_count) + desired = std::move(optimized); + else { + std::vector best = best_component_mix_weights_for_target_for_gcode(component_colors, target); + if (best.size() == component_count) + desired = std::move(best); + } + } + + if (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) { + const float v = clamp01f_for_gcode(desired[component_idx]); + weight_field.sample_component_weights[sample_idx * component_count + component_idx] = v; + fallback_acc[component_idx] += v * sample.weight; + } + fallback_weight += sample.weight; + } + + weight_field.fallback_weights.assign(component_count, 1.f / float(component_count)); + if (fallback_weight > EPSILON) { + for (size_t component_idx = 0; component_idx < component_count; ++component_idx) + weight_field.fallback_weights[component_idx] = + clamp01f_for_gcode(fallback_acc[component_idx] / fallback_weight); + } + + const float k_target_bucket_mm = high_resolution_texture_sampling ? 0.12f : 0.22f; + constexpr int k_min_bucket_dim = 16; + constexpr int k_max_bucket_dim = 320; + constexpr int k_max_buckets = 72000; + int bucket_width = std::clamp(int(std::ceil(span_x_mm / k_target_bucket_mm)) + 1, k_min_bucket_dim, k_max_bucket_dim); + int bucket_height = std::clamp(int(std::ceil(span_y_mm / k_target_bucket_mm)) + 1, k_min_bucket_dim, k_max_bucket_dim); + const int initial_buckets = bucket_width * bucket_height; + if (initial_buckets > k_max_buckets) { + const float scale_factor = std::sqrt(float(initial_buckets) / float(k_max_buckets)); + bucket_width = std::max(k_min_bucket_dim, int(std::ceil(float(bucket_width) / scale_factor))); + bucket_height = std::max(k_min_bucket_dim, int(std::ceil(float(bucket_height) / scale_factor))); + } + + weight_field.min_x_mm = min_x_mm; + weight_field.min_y_mm = min_y_mm; + weight_field.bucket_width = bucket_width; + weight_field.bucket_height = bucket_height; + weight_field.bucket_width_mm = std::max(1e-3f, span_x_mm / std::max(1, bucket_width - 1)); + weight_field.bucket_height_mm = std::max(1e-3f, span_y_mm / std::max(1, bucket_height - 1)); + weight_field.buckets.assign(size_t(bucket_width) * size_t(bucket_height), {}); + + for (size_t sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const float gx_unclamped = (weight_field.sample_x_mm[sample_idx] - min_x_mm) / weight_field.bucket_width_mm; + const float gy_unclamped = (weight_field.sample_y_mm[sample_idx] - min_y_mm) / weight_field.bucket_height_mm; + const int bx = std::clamp(int(std::floor(gx_unclamped)), 0, bucket_width - 1); + const int by = std::clamp(int(std::floor(gy_unclamped)), 0, bucket_height - 1); + const size_t bidx = size_t(by) * size_t(bucket_width) + size_t(bx); + weight_field.buckets[bidx].push_back(uint32_t(sample_idx)); + } + + return weight_field; +} + +static float sample_vertex_color_weight_field_for_gcode(const VertexColorOverhangWeightField &weight_field, + float x_mm, + float y_mm, + size_t component_idx, + bool high_resolution_texture_sampling, + bool compact_offset_mode = false) +{ + if (compact_offset_mode && !weight_field.empty() && component_idx < weight_field.component_count) { + std::vector values(weight_field.component_count, 0.f); + float max_value = 0.f; + for (size_t idx = 0; idx < weight_field.component_count; ++idx) { + values[idx] = sample_vertex_color_weight_field_for_gcode(weight_field, + x_mm, + y_mm, + idx, + high_resolution_texture_sampling, + false); + max_value = std::max(max_value, clamp01f_for_gcode(values[idx])); + } + if (max_value > EPSILON) + return clamp01f_for_gcode(values[component_idx] / max_value); + } + + const float fallback = component_idx < weight_field.fallback_weights.size() ? + weight_field.fallback_weights[component_idx] : 0.f; + if (weight_field.empty() || component_idx >= weight_field.component_count) + return fallback; + if (!std::isfinite(x_mm) || !std::isfinite(y_mm)) + return fallback; + + const float gx_unclamped = (x_mm - weight_field.min_x_mm) / std::max(weight_field.bucket_width_mm, 1e-6f); + const float gy_unclamped = (y_mm - weight_field.min_y_mm) / std::max(weight_field.bucket_height_mm, 1e-6f); + const int cx = std::clamp(int(std::floor(gx_unclamped)), 0, weight_field.bucket_width - 1); + const int cy = std::clamp(int(std::floor(gy_unclamped)), 0, weight_field.bucket_height - 1); + + const float sigma_scale = high_resolution_texture_sampling ? 0.45f : 0.7f; + const float min_sigma_mm = high_resolution_texture_sampling ? 0.04f : 0.06f; + const float sigma_x_mm = std::max(min_sigma_mm, weight_field.bucket_width_mm * sigma_scale); + const float sigma_y_mm = std::max(min_sigma_mm, weight_field.bucket_height_mm * sigma_scale); + const float inv_two_sigma_x2 = 1.f / std::max(2.f * sigma_x_mm * sigma_x_mm, 1e-8f); + const float inv_two_sigma_y2 = 1.f / std::max(2.f * sigma_y_mm * sigma_y_mm, 1e-8f); + + const float min_radius_mm = high_resolution_texture_sampling ? 0.16f : 0.30f; + const float radius_scale = high_resolution_texture_sampling ? 1.75f : 3.f; + const float max_radius_mm = std::max(min_radius_mm, std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * radius_scale); + const float max_radius2 = max_radius_mm * max_radius_mm; + const float min_bucket_span_mm = std::max(1e-3f, std::min(weight_field.bucket_width_mm, weight_field.bucket_height_mm)); + const int max_ring = std::max(1, int(std::ceil(max_radius_mm / min_bucket_span_mm))); + + float weighted_sum = 0.f; + float total_weight = 0.f; + size_t contributing_samples = 0; + + auto process_bucket = [&weight_field, + component_idx, + x_mm, + y_mm, + max_radius2, + inv_two_sigma_x2, + inv_two_sigma_y2, + &weighted_sum, + &total_weight, + &contributing_samples](int bx, int by) { + if (bx < 0 || by < 0 || bx >= weight_field.bucket_width || by >= weight_field.bucket_height) + return; + + const size_t bucket_idx = size_t(by) * size_t(weight_field.bucket_width) + size_t(bx); + if (bucket_idx >= weight_field.buckets.size()) + return; + + for (const uint32_t sample_idx_u32 : weight_field.buckets[bucket_idx]) { + const size_t sample_idx = size_t(sample_idx_u32); + if (sample_idx >= weight_field.sample_x_mm.size() || + sample_idx >= weight_field.sample_y_mm.size() || + sample_idx >= weight_field.sample_weight.size()) + continue; + + const float dx = x_mm - weight_field.sample_x_mm[sample_idx]; + const float dy = y_mm - weight_field.sample_y_mm[sample_idx]; + const float d2 = dx * dx + dy * dy; + if (d2 > max_radius2) + continue; + + const float kernel = std::exp(-(dx * dx) * inv_two_sigma_x2 - (dy * dy) * inv_two_sigma_y2); + const float sample_w = weight_field.sample_weight[sample_idx] * kernel; + if (!std::isfinite(sample_w) || sample_w <= EPSILON) + continue; + + const size_t value_idx = sample_idx * weight_field.component_count + component_idx; + if (value_idx >= weight_field.sample_component_weights.size()) + continue; + + weighted_sum += weight_field.sample_component_weights[value_idx] * sample_w; + total_weight += sample_w; + ++contributing_samples; + } + }; + + for (int ring = 0; ring <= max_ring; ++ring) { + const int min_x = std::max(0, cx - ring); + const int max_x = std::min(weight_field.bucket_width - 1, cx + ring); + const int min_y = std::max(0, cy - ring); + const int max_y = std::min(weight_field.bucket_height - 1, cy + ring); + + if (ring == 0) { + process_bucket(cx, cy); + } else { + for (int x = min_x; x <= max_x; ++x) { + process_bucket(x, min_y); + if (max_y != min_y) + process_bucket(x, max_y); + } + for (int y = min_y + 1; y <= max_y - 1; ++y) { + process_bucket(min_x, y); + if (max_x != min_x) + process_bucket(max_x, y); + } + } + + if (total_weight > EPSILON && contributing_samples >= 12) + break; + } + + if (total_weight > EPSILON) + return clamp01f_for_gcode(weighted_sum / total_weight); + + float nearest_d2 = std::numeric_limits::max(); + float nearest_value = fallback; + const int nearest_ring_limit = std::min(std::max(max_ring + 2, 4), 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, component_idx, x_mm, y_mm, &nearest_d2, &nearest_value](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 + component_idx; + if (value_idx >= weight_field.sample_component_weights.size()) + continue; + + nearest_d2 = d2; + nearest_value = weight_field.sample_component_weights[value_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_d2 < std::numeric_limits::max() && ring >= 2) + break; + } + + if (nearest_d2 < std::numeric_limits::max()) + return clamp01f_for_gcode(nearest_value); + + return fallback; +} + +static float component_angular_influence_for_gcode(unsigned int active_component_id, + float theta_deg, + const std::vector &component_ids, + const std::vector &component_angles_deg) +{ + if (component_ids.empty() || component_ids.size() != component_angles_deg.size()) + return 0.f; + + const auto active_it = std::find(component_ids.begin(), component_ids.end(), active_component_id); + if (active_it == component_ids.end()) + return 0.f; + + if (component_ids.size() == 1) + return 1.f; + + struct SortedComponentAngle { + float angle_deg { 0.f }; + size_t component_idx { 0 }; + }; + + std::vector sorted_angles; + sorted_angles.reserve(component_ids.size()); + for (size_t i = 0; i < component_ids.size(); ++i) + sorted_angles.push_back({ normalize_angle_deg_for_gcode(component_angles_deg[i]), i }); + + std::sort(sorted_angles.begin(), sorted_angles.end(), [](const SortedComponentAngle &lhs, const SortedComponentAngle &rhs) { + return lhs.angle_deg < rhs.angle_deg; + }); + + const size_t active_component_idx = size_t(active_it - component_ids.begin()); + const auto sorted_active_it = std::find_if(sorted_angles.begin(), sorted_angles.end(), + [active_component_idx](const SortedComponentAngle &entry) { + return entry.component_idx == active_component_idx; + }); + if (sorted_active_it == sorted_angles.end()) + return 0.f; + + const size_t sorted_pos = size_t(sorted_active_it - sorted_angles.begin()); + const size_t count = sorted_angles.size(); + const float prev_angle = sorted_angles[(sorted_pos + count - 1) % count].angle_deg; + const float self_angle = sorted_angles[sorted_pos].angle_deg; + const float next_angle = sorted_angles[(sorted_pos + 1) % count].angle_deg; + const float prev_to_self_deg = angular_distance_cw_deg_for_gcode(prev_angle, self_angle); + const float self_to_next_deg = angular_distance_cw_deg_for_gcode(self_angle, next_angle); + + if (prev_to_self_deg <= 1e-3f || self_to_next_deg <= 1e-3f) { + float total_weight = 0.f; + float active_weight = 0.f; + for (size_t i = 0; i < component_ids.size(); ++i) { + const float dist = angular_distance_deg_for_gcode(theta_deg, component_angles_deg[i]); + const float weight = std::max(0.f, 1.f - dist / 180.f); + total_weight += weight; + if (component_ids[i] == active_component_id) + active_weight += weight; + } + + if (total_weight <= EPSILON) + return 0.f; + return std::clamp(active_weight / total_weight, 0.f, 1.f); + } + + const float theta_norm = normalize_angle_deg_for_gcode(theta_deg); + const float prev_to_theta_deg = angular_distance_cw_deg_for_gcode(prev_angle, theta_norm); + if (prev_to_theta_deg <= prev_to_self_deg + 1e-4f) + return std::clamp(prev_to_theta_deg / prev_to_self_deg, 0.f, 1.f); + + const float self_to_theta_deg = angular_distance_cw_deg_for_gcode(self_angle, theta_norm); + if (self_to_theta_deg <= self_to_next_deg + 1e-4f) + return std::clamp(1.f - self_to_theta_deg / self_to_next_deg, 0.f, 1.f); + + return 0.f; +} + +std::optional GCode::texture_mapping_seam_hiding_point(const ExtrusionLoop &) +{ + return std::nullopt; +} + std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, double speed, const ExtrusionEntitiesPtr& region_perimeters, const Point* start_point) { @@ -6182,6 +7650,46 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, { std::string gcode; + struct OuterWallGradientSegmentMod { + double flow_scale { 1.0 }; + coord_t shift_dx { 0 }; + coord_t shift_dy { 0 }; + double shift_unit_x { 0.0 }; + double shift_unit_y { 0.0 }; + double length_mm { 0.0 }; + float centerline_shift_mm { 0.f }; + float balance_weight { 0.f }; + }; + + struct OuterWallGradientDynamicContext { + bool enabled { false }; + bool vertex_color_match_mode { false }; + bool high_resolution_texture_sampling { false }; + bool nonlinear_offset_adjustment { false }; + bool compact_offset_mode { false }; + bool object_center_mode { false }; + Point object_center; + unsigned int active_component_id { 0 }; + size_t active_component_idx { size_t(-1) }; + const VertexColorOverhangWeightField *vertex_color_weight_field { nullptr }; + std::vector component_ids; + std::vector component_distances_mm; + std::vector rotated_angles; + float inset_strength_reference_mm { 0.f }; + float fade_factor { 0.f }; + float signed_fade_factor { 1.f }; + float max_width_delta_mm { 0.f }; + float active_component_strength_factor { 1.f }; + float active_component_minimum_offset_factor { 0.f }; + float base_outer_width_mm { 0.4f }; + float flow_reference_width_mm { 0.4f }; + float base_centerline_shift_mm { 0.f }; + float centerline_shift_balance_mm { 0.f }; + float centerline_shift_balance_weight_scale { 0.f }; + float layer_height_mm { 0.2f }; + float sagging_ratio { 0.f }; + }; + if (is_bridge(path.role())) description += " (bridge)"; @@ -6192,6 +7700,432 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, return lerp(m_nominal_z - height, m_nominal_z, z_ratio); }; + auto make_shifted_point = [](const Point &p, coord_t dx, coord_t dy) { + return Point(coord_t(p.x() + dx), coord_t(p.y() + dy)); + }; + auto can_emit_extrusion_delta = [](double dE) { + return std::isfinite(dE); + }; + auto can_emit_sloped_extrusion = [](const Vec3d &dest, double dE) { + return dest.allFinite() && std::isfinite(dE); + }; + + std::vector outer_wall_gradient_segment_mods; + OuterWallGradientDynamicContext outer_wall_gradient_dynamic_ctx; + bool outer_wall_gradient_modulated_path = false; + Point outer_wall_gradient_start_point = path.first_point(); + + if (!path.is_force_no_extrusion() && + is_external_perimeter(path.role()) && + m_curr_print != nullptr && + m_writer.filament() != nullptr && + path.polyline.points.size() >= 2) { + const size_t num_physical = m_config.filament_colour.values.size(); + const unsigned int texture_zone_id = unsigned(std::max(0, m_config.wall_filament.value)); + const TextureMappingManager &texture_mgr = m_curr_print->texture_mapping_manager(); + if (num_physical > 0 && texture_zone_id > 0 && texture_mgr.is_texture_mapping_zone_id(texture_zone_id)) { + const TextureMappingZone *zone = texture_mgr.zone_from_id(texture_zone_id); + const bool vertex_color_match_mode = zone != nullptr && is_vertex_color_match_overhang_row_for_gcode(*zone); + if (zone != nullptr && + is_horizontal_overhang_gradient_row_for_gcode(*zone) && + (vertex_color_match_mode || + is_2d_offset_gradient_row_for_gcode(*zone) || + has_explicit_offset_gradient_profile_for_gcode(*zone))) { + std::vector component_ids = decode_offset_component_ids_for_gcode(*zone, num_physical); + if (vertex_color_match_mode) { + if (!m_warned_texture_mapping_filament_count_mismatch && + TextureMappingManager::component_count_mismatch(*zone, num_physical)) { + m_warned_texture_mapping_filament_count_mismatch = true; + m_curr_print->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _(L("A texture mapping zone has a filament count that does not match its selected color mode. Slicing will choose fallback filaments for the missing or extra color channels."))); + } + + const std::vector effective_component_ids = + TextureMappingManager::effective_texture_component_ids(*zone, num_physical, m_config.filament_colour.values); + if (!effective_component_ids.empty()) + component_ids = effective_component_ids; + } + if (!component_ids.empty()) { + const unsigned int active_component_id = unsigned(m_writer.filament()->id() + 1); + const auto active_component_it = std::find(component_ids.begin(), component_ids.end(), active_component_id); + if (active_component_it != component_ids.end()) { + std::vector reference_nozzles; + reference_nozzles.reserve(component_ids.size() + 2); + auto append_nozzle = [&reference_nozzles, this](unsigned int component_id) { + if (component_id == 0) + return; + const size_t idx = size_t(component_id - 1); + if (idx < m_config.nozzle_diameter.values.size()) + reference_nozzles.emplace_back(float(m_config.nozzle_diameter.get_at(idx))); + }; + for (unsigned int id : component_ids) + append_nozzle(id); + append_nozzle(zone->component_a); + append_nozzle(zone->component_b); + + const float reference_nozzle = reference_nozzles.empty() ? + float(m_config.nozzle_diameter.values.empty() ? 0.4 : m_config.nozzle_diameter.values.front()) : + std::accumulate(reference_nozzles.begin(), reference_nozzles.end(), 0.f) / float(reference_nozzles.size()); + const float max_allowed_distance_mm = TextureMappingManager::max_component_surface_offset_mm(reference_nozzle); + + std::vector distances_mm = TextureMappingManager::effective_offset_distances(*zone, component_ids.size(), reference_nozzle); + std::vector angles_deg = TextureMappingManager::effective_offset_angles(*zone, component_ids.size()); + if (distances_mm.size() != component_ids.size()) + distances_mm.assign(component_ids.size(), 0.f); + if (angles_deg.size() != component_ids.size()) + angles_deg = TextureMappingManager::default_offset_angles(component_ids.size()); + for (float &a : angles_deg) + a = normalize_angle_deg_for_gcode(a); + + bool has_nonzero_distance = false; + if (vertex_color_match_mode) { + distances_mm.assign(component_ids.size(), max_allowed_distance_mm); + has_nonzero_distance = max_allowed_distance_mm > EPSILON; + } else { + for (float &d : distances_mm) { + d = std::clamp(d, 0.f, max_allowed_distance_mm); + has_nonzero_distance = has_nonzero_distance || (d > EPSILON); + } + } + + if (has_nonzero_distance) { + const PrintObject *layer_object = m_layer ? m_layer->object() : nullptr; + const int object_layer_count = layer_object ? int(layer_object->layer_count()) : 0; + const int current_layer_index = m_layer ? int(m_layer->id()) : 0; + const float z_progress = object_layer_count > 1 ? + std::clamp(float(current_layer_index) / float(object_layer_count - 1), 0.f, 1.f) : 0.f; + + float rotation_deg = 0.f; + if (zone->offset_rotation_enabled) { + const float repeated = repeated_rotation_progress_for_gcode(z_progress, std::max(1.f, zone->offset_repeats), zone->offset_reverse_repeats); + const float direction = zone->offset_clockwise ? -1.f : 1.f; + rotation_deg = direction * 360.f * zone->offset_rotations * repeated; + } + + const float signed_fade_factor = offset_fade_factor_for_gcode(zone->offset_fade_mode, z_progress); + const float fade_factor = std::abs(signed_fade_factor); + + std::vector rotated_angles = angles_deg; + for (float &a : rotated_angles) + a = normalize_angle_deg_for_gcode(a + rotation_deg); + + const size_t active_component_idx = size_t(active_component_it - component_ids.begin()); + const float active_component_strength_factor = overhang_filament_strength_factor_for_gcode(*zone, active_component_id); + const float active_component_minimum_offset_factor = overhang_filament_minimum_offset_factor_for_gcode(*zone, active_component_id); + const float max_component_distance_mm = *std::max_element(distances_mm.begin(), distances_mm.end()); + const float path_outer_width_mm = std::max( + 0.01f, + path.width > EPSILON ? path.width : float(m_config.outer_wall_line_width.get_abs_value(reference_nozzle))); + const float texture_mapping_max_outer_width_mm = std::max( + 0.05f, + float(m_config.texture_mapping_outer_wall_gradient_max_line_width.value)); + const float base_outer_width_mm = vertex_color_match_mode ? texture_mapping_max_outer_width_mm : path_outer_width_mm; + const float flow_reference_width_mm = path_outer_width_mm; + const float base_centerline_shift_mm = vertex_color_match_mode ? 0.5f * (base_outer_width_mm - flow_reference_width_mm) : 0.f; + const float config_min_gradient_width_mm = std::clamp( + float(m_config.texture_mapping_outer_wall_gradient_min_line_width.value), + 0.05f, + base_outer_width_mm); + const float layer_height_mm = std::max( + 0.01f, + path.height > EPSILON ? path.height : float(m_layer == nullptr ? m_last_height : m_layer->height)); + const float min_width_for_positive_spacing_mm = layer_height_mm * float(1. - 0.25 * PI) + 1e-4f; + const float safe_min_gradient_width_mm = std::clamp( + std::max(config_min_gradient_width_mm, min_width_for_positive_spacing_mm), + 0.05f, + base_outer_width_mm); + const float max_width_delta_mm = std::max(0.f, base_outer_width_mm - safe_min_gradient_width_mm); + const float global_strength_factor = std::clamp( + float(m_config.texture_mapping_outer_wall_gradient_global_strength.value) / 100.f, + 0.f, + 1.f); + const float effective_max_width_delta_mm = max_width_delta_mm * global_strength_factor; + const bool object_center_mode = + !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 nonlinear_offset_adjustment = zone->nonlinear_offset_adjustment; + const bool compact_offset_mode = zone->compact_offset_mode; + 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) : + std::max(0.12f, layer_height_mm * 1.5f); + const int texture_filament_color_mode = std::clamp( + zone->filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + const bool texture_force_sequential_filaments = zone->force_sequential_filaments; + const float texture_contrast_pct = std::clamp(zone->contrast_pct, 25.f, 300.f); + const float texture_tone_gamma = + (!std::isfinite(zone->tone_gamma) || zone->tone_gamma <= 0.f) ? + 1.f : + std::clamp(zone->tone_gamma, 0.5f, 3.f); + const float texture_sagging_ratio = + std::isfinite(zone->sagging_ratio) ? std::clamp(zone->sagging_ratio, 0.f, 6.f) : 0.f; + + const VertexColorOverhangWeightField *vertex_color_weight_field = nullptr; + if (vertex_color_match_mode && layer_object != nullptr) { + const bool raw_texture_mapping_mode = + zone->texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues); + std::vector> component_colors; + component_colors.reserve(component_ids.size()); + bool missing_component_color = false; + for (const unsigned int id : component_ids) { + if (id < 1 || id > m_config.filament_colour.values.size()) { + if (raw_texture_mapping_mode) + component_colors.push_back({ 0.f, 0.f, 0.f }); + else + missing_component_color = true; + continue; + } + ColorRGB decoded; + if (!decode_color(m_config.filament_colour.get_at(size_t(id - 1)), decoded)) { + if (raw_texture_mapping_mode) + component_colors.push_back({ 0.f, 0.f, 0.f }); + else + missing_component_color = true; + continue; + } + component_colors.push_back({ decoded.r(), decoded.g(), decoded.b() }); + } + if (!missing_component_color && component_colors.size() == component_ids.size() && !component_colors.empty()) { + std::ostringstream component_key_stream; + for (size_t idx = 0; idx < component_ids.size(); ++idx) { + if (idx > 0) + component_key_stream << '/'; + component_key_stream << component_ids[idx]; + } + component_key_stream << (raw_texture_mapping_mode ? "|raw" : "|blend"); + component_key_stream << "|fc" << texture_filament_color_mode; + component_key_stream << "|fs" << (texture_force_sequential_filaments ? 1 : 0); + component_key_stream << "|ct" << int(std::lround(texture_contrast_pct)); + component_key_stream << "|tg" << int(std::lround(texture_tone_gamma * 100.f)); + component_key_stream << "|hr" << (high_resolution_texture_sampling ? 1 : 0); + if (m_layer != nullptr) + component_key_stream << "|L" << m_layer->id(); + const auto cache_key = std::make_tuple(layer_object, texture_zone_id, component_key_stream.str()); + auto cache_it = m_vertex_color_overhang_weight_field_cache.find(cache_key); + if (cache_it == m_vertex_color_overhang_weight_field_cache.end()) { + cache_it = m_vertex_color_overhang_weight_field_cache + .emplace(cache_key, + build_vertex_color_weight_field_for_gcode(*layer_object, + component_colors, + raw_texture_mapping_mode, + texture_filament_color_mode, + texture_force_sequential_filaments, + texture_contrast_pct, + texture_tone_gamma, + use_layer_aware_weighting, + layer_sample_z_mm, + layer_sample_falloff_mm, + high_resolution_texture_sampling)) + .first; + } + if (!cache_it->second.empty()) + vertex_color_weight_field = &cache_it->second; + } + } + + const bool has_vertex_color_weight_field = + vertex_color_weight_field != nullptr && !vertex_color_weight_field->empty(); + + if (fade_factor > EPSILON && max_component_distance_mm > EPSILON && effective_max_width_delta_mm > EPSILON && + (!vertex_color_match_mode || has_vertex_color_weight_field)) { + Point object_center = layer_object ? layer_object->bounding_box().center() : + Point(coord_t((int64_t(path.first_point().x()) + int64_t(path.last_point().x())) / 2), + coord_t((int64_t(path.first_point().y()) + int64_t(path.last_point().y())) / 2)); + + 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.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; + outer_wall_gradient_dynamic_ctx.object_center = object_center; + outer_wall_gradient_dynamic_ctx.active_component_id = active_component_id; + outer_wall_gradient_dynamic_ctx.active_component_idx = active_component_idx; + outer_wall_gradient_dynamic_ctx.vertex_color_weight_field = vertex_color_weight_field; + outer_wall_gradient_dynamic_ctx.component_ids = component_ids; + outer_wall_gradient_dynamic_ctx.component_distances_mm = distances_mm; + outer_wall_gradient_dynamic_ctx.rotated_angles = rotated_angles; + outer_wall_gradient_dynamic_ctx.inset_strength_reference_mm = max_allowed_distance_mm; + outer_wall_gradient_dynamic_ctx.fade_factor = fade_factor; + outer_wall_gradient_dynamic_ctx.signed_fade_factor = signed_fade_factor; + outer_wall_gradient_dynamic_ctx.max_width_delta_mm = effective_max_width_delta_mm; + outer_wall_gradient_dynamic_ctx.active_component_strength_factor = active_component_strength_factor; + outer_wall_gradient_dynamic_ctx.active_component_minimum_offset_factor = active_component_minimum_offset_factor; + outer_wall_gradient_dynamic_ctx.base_outer_width_mm = base_outer_width_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; + outer_wall_gradient_dynamic_ctx.sagging_ratio = texture_sagging_ratio; + + outer_wall_gradient_segment_mods.reserve(path.polyline.points.size() - 1); + + float max_width_delta_limit_mm = std::min(effective_max_width_delta_mm, 2.f * max_allowed_distance_mm); + if (texture_sagging_ratio > EPSILON) + max_width_delta_limit_mm = std::min(max_width_delta_limit_mm, layer_height_mm * texture_sagging_ratio); + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= EPSILON) + outer_wall_gradient_dynamic_ctx.enabled = false; + + auto apply_centerline_shift_to_mod = [&](OuterWallGradientSegmentMod &mod, float centerline_shift_mm) { + mod.shift_dx = 0; + mod.shift_dy = 0; + mod.centerline_shift_mm = centerline_shift_mm; + if (std::abs(centerline_shift_mm) <= EPSILON) + return true; + + const bool reverse_shift = (signed_fade_factor < 0.f) != (centerline_shift_mm < 0.f); + const double shift_x = reverse_shift ? -mod.shift_unit_x : mod.shift_unit_x; + const double shift_y = reverse_shift ? -mod.shift_unit_y : mod.shift_unit_y; + const double shift_scaled = scale_(double(std::abs(centerline_shift_mm))); + if (!std::isfinite(shift_x) || !std::isfinite(shift_y) || !std::isfinite(shift_scaled)) + return false; + const coord_t max_shift_coord = scale_(std::max(0.5f, base_outer_width_mm)); + return clamped_shift_coord_for_gcode(shift_x, shift_scaled, max_shift_coord, mod.shift_dx) && + clamped_shift_coord_for_gcode(shift_y, shift_scaled, max_shift_coord, mod.shift_dy); + }; + + for (const Line &line : path.polyline.lines()) { + if (!outer_wall_gradient_dynamic_ctx.enabled) + break; + + OuterWallGradientSegmentMod mod; + const double ax = double(line.a.x()); + const double ay = double(line.a.y()); + const double bx = double(line.b.x()); + const double by = double(line.b.y()); + const double dx = bx - ax; + const double dy = by - ay; + const double len = std::hypot(dx, dy); + mod.length_mm = unscale(len); + if (len <= EPSILON) { + outer_wall_gradient_segment_mods.emplace_back(mod); + continue; + } + + const double mid_x = 0.5 * (ax + bx); + const double mid_y = 0.5 * (ay + by); + const double radial_x = mid_x - double(object_center.x()); + const double radial_y = mid_y - double(object_center.y()); + + const Point mid_point(coord_t(std::llround(mid_x)), coord_t(std::llround(mid_y))); + double outward_x = 0.0; + double outward_y = 0.0; + resolve_segment_shift_outward_normal_for_gcode(m_layer, mid_point, dx, dy, len, radial_x, radial_y, outward_x, outward_y); + + double theta_direction_x = outward_x; + double theta_direction_y = outward_y; + if (object_center_mode) { + const double radial_len = std::hypot(radial_x, radial_y); + if (radial_len > EPSILON) { + theta_direction_x = radial_x / radial_len; + theta_direction_y = radial_y / radial_len; + } + } + + const float theta_deg = normalize_angle_deg_for_gcode(float(Geometry::rad2deg(std::atan2(theta_direction_y, theta_direction_x)))); + float inset_strength = 0.f; + if (vertex_color_match_mode) { + if (vertex_color_weight_field != nullptr && + active_component_idx < component_ids.size() && + !vertex_color_weight_field->empty()) { + const float mid_x_mm = 0.5f * (unscale(line.a.x()) + unscale(line.b.x())); + const float mid_y_mm = 0.5f * (unscale(line.a.y()) + unscale(line.b.y())); + const float desired_strength = sample_vertex_color_weight_field_for_gcode( + *vertex_color_weight_field, + mid_x_mm, + mid_y_mm, + active_component_idx, + high_resolution_texture_sampling, + compact_offset_mode); + inset_strength = std::clamp(1.f - desired_strength, 0.f, 1.f); + } + } else { + float raw_inset_mm = 0.f; + for (size_t i = 0; i < component_ids.size(); ++i) { + if (i == active_component_idx) + continue; + const float influence = component_angular_influence_for_gcode(component_ids[i], + theta_deg, + component_ids, + rotated_angles); + raw_inset_mm += distances_mm[i] * influence; + } + inset_strength = std::clamp(raw_inset_mm / std::max(max_allowed_distance_mm, float(EPSILON)), 0.f, 1.f); + } + inset_strength = std::clamp(inset_strength * fade_factor, 0.f, 1.f); + const float stair_step_mm = nonlinear_offset_adjustment ? + local_surface_stair_step_distance_for_gcode(m_layer, + mid_point, + outward_x, + outward_y, + base_outer_width_mm, + max_allowed_distance_mm) : + std::numeric_limits::quiet_NaN(); + const float variable_width_delta_mm = variable_width_delta_for_visibility_range_for_gcode( + inset_strength, + max_width_delta_limit_mm, + active_component_minimum_offset_factor, + active_component_strength_factor, + nonlinear_offset_adjustment, + layer_height_mm, + stair_step_mm, + texture_sagging_ratio); + const float width_delta_mm = std::clamp(variable_width_delta_mm, 0.f, max_width_delta_limit_mm); + if (!std::isfinite(width_delta_mm) || !std::isfinite(base_outer_width_mm) || !std::isfinite(layer_height_mm)) { + outer_wall_gradient_segment_mods.emplace_back(mod); + continue; + } + + const float target_width_mm = base_outer_width_mm - width_delta_mm; + if (!std::isfinite(target_width_mm) || target_width_mm <= 0.f) { + outer_wall_gradient_segment_mods.emplace_back(mod); + continue; + } + + mod.flow_scale = flow_scale_for_target_width_for_gcode(flow_reference_width_mm, target_width_mm, layer_height_mm); + if (!std::isfinite(mod.flow_scale) || mod.flow_scale <= 0.) { + outer_wall_gradient_segment_mods.emplace_back(OuterWallGradientSegmentMod{}); + continue; + } + + const float centerline_shift_mm = base_centerline_shift_mm + 0.5f * width_delta_mm; + mod.shift_unit_x = -outward_x; + mod.shift_unit_y = -outward_y; + mod.balance_weight = max_width_delta_limit_mm > EPSILON ? + std::clamp(width_delta_mm / max_width_delta_limit_mm, 0.f, 1.f) : + 0.f; + if (!apply_centerline_shift_to_mod(mod, centerline_shift_mm)) { + outer_wall_gradient_segment_mods.emplace_back(OuterWallGradientSegmentMod{}); + continue; + } + outer_wall_gradient_segment_mods.emplace_back(mod); + } + + outer_wall_gradient_modulated_path = std::any_of( + outer_wall_gradient_segment_mods.begin(), + outer_wall_gradient_segment_mods.end(), + [](const OuterWallGradientSegmentMod &mod) { + return mod.shift_dx != 0 || mod.shift_dy != 0 || std::abs(mod.flow_scale - 1.0) > 1e-6; + }); + + if (outer_wall_gradient_modulated_path && !outer_wall_gradient_segment_mods.empty()) { + const OuterWallGradientSegmentMod &start_mod = outer_wall_gradient_segment_mods.front(); + outer_wall_gradient_start_point = make_shifted_point(path.first_point(), start_mod.shift_dx, start_mod.shift_dy); + } + } + } + } + } + } + } + } + + const Point extrusion_start_point = outer_wall_gradient_modulated_path ? outer_wall_gradient_start_point : path.first_point(); + bool slope_need_z_travel = false; if (sloped != nullptr && !sloped->is_flat()) { auto target_z = get_sloped_z(sloped->slope_begin.z_ratio); @@ -6200,10 +8134,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Move to first point of extrusion path // path is 2D. But in slope lift case, lift z is done in travel_to function. // Add m_need_change_layer_lift_z when change_layer in case of no lift if m_last_pos is equal to path.first_point() by chance - if (!m_last_pos_defined || m_last_pos != path.first_point() || m_need_change_layer_lift_z || slope_need_z_travel) { + if (!m_last_pos_defined || m_last_pos != extrusion_start_point || m_need_change_layer_lift_z || slope_need_z_travel) { const bool _last_pos_undefined = !m_last_pos_defined; gcode += this->travel_to( - path.first_point(), + extrusion_start_point, path.role(), "move to first " + description + " point", sloped == nullptr ? DBL_MAX : get_sloped_z(sloped->slope_begin.z_ratio) @@ -6735,6 +8669,171 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, ironing_fan_speed >= 0 && path.role() == erIroning); }; + auto segment_modulation_at = [&outer_wall_gradient_segment_mods, outer_wall_gradient_modulated_path](size_t idx) { + if (!outer_wall_gradient_modulated_path || idx >= outer_wall_gradient_segment_mods.size()) + return OuterWallGradientSegmentMod{}; + return outer_wall_gradient_segment_mods[idx]; + }; + + const Layer *surface_layer = m_layer; + + auto dynamic_modulation_for_line = [&outer_wall_gradient_dynamic_ctx, surface_layer](const Line &line) { + OuterWallGradientSegmentMod mod; + if (!outer_wall_gradient_dynamic_ctx.enabled) + return mod; + + const double ax = double(line.a.x()); + const double ay = double(line.a.y()); + const double bx = double(line.b.x()); + const double by = double(line.b.y()); + const double dx = bx - ax; + const double dy = by - ay; + const double len = std::hypot(dx, dy); + if (len <= EPSILON) + return mod; + + const double mid_x = 0.5 * (ax + bx); + const double mid_y = 0.5 * (ay + by); + const double radial_x = mid_x - double(outer_wall_gradient_dynamic_ctx.object_center.x()); + const double radial_y = mid_y - double(outer_wall_gradient_dynamic_ctx.object_center.y()); + + const Point mid_point(coord_t(std::llround(mid_x)), coord_t(std::llround(mid_y))); + double outward_x = 0.0; + double outward_y = 0.0; + resolve_segment_shift_outward_normal_for_gcode(surface_layer, + mid_point, + dx, + dy, + len, + radial_x, + radial_y, + outward_x, + outward_y); + + double theta_direction_x = outward_x; + double theta_direction_y = outward_y; + if (outer_wall_gradient_dynamic_ctx.object_center_mode) { + const double radial_len = std::hypot(radial_x, radial_y); + if (radial_len > EPSILON) { + theta_direction_x = radial_x / radial_len; + theta_direction_y = radial_y / radial_len; + } + } + + const float theta_deg = normalize_angle_deg_for_gcode(float(Geometry::rad2deg(std::atan2(theta_direction_y, theta_direction_x)))); + float inset_strength = 0.f; + if (outer_wall_gradient_dynamic_ctx.vertex_color_match_mode) { + if (outer_wall_gradient_dynamic_ctx.vertex_color_weight_field != nullptr && + !outer_wall_gradient_dynamic_ctx.vertex_color_weight_field->empty() && + outer_wall_gradient_dynamic_ctx.active_component_idx < + outer_wall_gradient_dynamic_ctx.vertex_color_weight_field->component_count) { + const float mid_x_mm = 0.5f * (unscale(line.a.x()) + unscale(line.b.x())); + const float mid_y_mm = 0.5f * (unscale(line.a.y()) + unscale(line.b.y())); + const float desired_strength = sample_vertex_color_weight_field_for_gcode( + *outer_wall_gradient_dynamic_ctx.vertex_color_weight_field, + mid_x_mm, + mid_y_mm, + outer_wall_gradient_dynamic_ctx.active_component_idx, + outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling, + outer_wall_gradient_dynamic_ctx.compact_offset_mode); + inset_strength = std::clamp(1.f - desired_strength, 0.f, 1.f); + } + } else { + float raw_inset_mm = 0.f; + const size_t component_count = std::min(outer_wall_gradient_dynamic_ctx.component_ids.size(), + outer_wall_gradient_dynamic_ctx.component_distances_mm.size()); + for (size_t i = 0; i < component_count; ++i) { + if (i == outer_wall_gradient_dynamic_ctx.active_component_idx) + continue; + const float influence = component_angular_influence_for_gcode(outer_wall_gradient_dynamic_ctx.component_ids[i], + theta_deg, + outer_wall_gradient_dynamic_ctx.component_ids, + outer_wall_gradient_dynamic_ctx.rotated_angles); + raw_inset_mm += outer_wall_gradient_dynamic_ctx.component_distances_mm[i] * influence; + } + inset_strength = std::clamp( + raw_inset_mm / std::max(outer_wall_gradient_dynamic_ctx.inset_strength_reference_mm, float(EPSILON)), + 0.f, + 1.f); + } + inset_strength = std::clamp(inset_strength * outer_wall_gradient_dynamic_ctx.fade_factor, 0.f, 1.f); + float max_width_delta_limit_mm = std::min( + outer_wall_gradient_dynamic_ctx.max_width_delta_mm, + 2.f * outer_wall_gradient_dynamic_ctx.inset_strength_reference_mm); + if (outer_wall_gradient_dynamic_ctx.sagging_ratio > EPSILON) + max_width_delta_limit_mm = std::min(max_width_delta_limit_mm, + outer_wall_gradient_dynamic_ctx.layer_height_mm * + outer_wall_gradient_dynamic_ctx.sagging_ratio); + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= EPSILON) + return OuterWallGradientSegmentMod{}; + const float stair_step_mm = outer_wall_gradient_dynamic_ctx.nonlinear_offset_adjustment ? + local_surface_stair_step_distance_for_gcode(surface_layer, + mid_point, + outward_x, + outward_y, + outer_wall_gradient_dynamic_ctx.base_outer_width_mm, + outer_wall_gradient_dynamic_ctx.inset_strength_reference_mm) : + std::numeric_limits::quiet_NaN(); + const float variable_width_delta_mm = variable_width_delta_for_visibility_range_for_gcode( + inset_strength, + max_width_delta_limit_mm, + outer_wall_gradient_dynamic_ctx.active_component_minimum_offset_factor, + outer_wall_gradient_dynamic_ctx.active_component_strength_factor, + outer_wall_gradient_dynamic_ctx.nonlinear_offset_adjustment, + outer_wall_gradient_dynamic_ctx.layer_height_mm, + stair_step_mm, + outer_wall_gradient_dynamic_ctx.sagging_ratio); + const float width_delta_mm = std::clamp(variable_width_delta_mm, 0.f, max_width_delta_limit_mm); + if (!std::isfinite(width_delta_mm) || + !std::isfinite(outer_wall_gradient_dynamic_ctx.base_outer_width_mm) || + !std::isfinite(outer_wall_gradient_dynamic_ctx.layer_height_mm)) + return OuterWallGradientSegmentMod{}; + + const float target_width_mm = outer_wall_gradient_dynamic_ctx.base_outer_width_mm - width_delta_mm; + if (!std::isfinite(target_width_mm) || target_width_mm <= 0.f) + return OuterWallGradientSegmentMod{}; + + mod.flow_scale = flow_scale_for_target_width_for_gcode(outer_wall_gradient_dynamic_ctx.flow_reference_width_mm, + target_width_mm, + outer_wall_gradient_dynamic_ctx.layer_height_mm); + if (!std::isfinite(mod.flow_scale) || mod.flow_scale <= 0.) + return OuterWallGradientSegmentMod{}; + + const float balance_weight = + max_width_delta_limit_mm > EPSILON ? std::clamp(width_delta_mm / max_width_delta_limit_mm, 0.f, 1.f) : 0.f; + const float raw_centerline_shift_mm = + outer_wall_gradient_dynamic_ctx.base_centerline_shift_mm + + 0.5f * width_delta_mm + + outer_wall_gradient_dynamic_ctx.centerline_shift_balance_mm * + balance_weight * + outer_wall_gradient_dynamic_ctx.centerline_shift_balance_weight_scale; + const float centerline_shift_mm = + outer_wall_gradient_dynamic_ctx.centerline_shift_balance_mm == 0.f ? + raw_centerline_shift_mm : + std::clamp(raw_centerline_shift_mm, + 0.f, + outer_wall_gradient_dynamic_ctx.base_centerline_shift_mm + 0.5f * max_width_delta_limit_mm); + if (std::abs(centerline_shift_mm) > EPSILON) { + const double inward_x = -outward_x; + const double inward_y = -outward_y; + const bool reverse_shift = + (outer_wall_gradient_dynamic_ctx.signed_fade_factor < 0.f) != (centerline_shift_mm < 0.f); + const double shift_x = reverse_shift ? -inward_x : inward_x; + const double shift_y = reverse_shift ? -inward_y : inward_y; + const double shift_scaled = scale_(double(std::abs(centerline_shift_mm))); + if (!std::isfinite(shift_x) || !std::isfinite(shift_y) || !std::isfinite(shift_scaled)) + return OuterWallGradientSegmentMod{}; + const coord_t max_shift_coord = scale_(std::max(0.5f, outer_wall_gradient_dynamic_ctx.base_outer_width_mm)); + if (!clamped_shift_coord_for_gcode(shift_x, shift_scaled, max_shift_coord, mod.shift_dx) || + !clamped_shift_coord_for_gcode(shift_y, shift_scaled, max_shift_coord, mod.shift_dy)) + return OuterWallGradientSegmentMod{}; + } + + return mod; + }; + + Point emitted_last_point = extrusion_start_point; + if (!variable_speed) { // F is mm per minute. if( (std::abs(writer().get_current_speed() - F) > EPSILON) || (std::abs(_mm3_per_mm - m_last_mm3_mm) > EPSILON) ){ @@ -6803,39 +8902,138 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion // Attention: G2 and G3 is not supported in spiral_mode mode - if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr) { + if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || outer_wall_gradient_modulated_path) { + constexpr double k_max_reasonable_segment_mm = 2000.0; double path_length = 0.; double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR; + size_t line_idx = 0; for (const Line& line : path.polyline.lines()) { + const size_t segment_idx = line_idx++; std::string tempDescription = description; const double line_length = line.length() * SCALING_FACTOR; + if (!std::isfinite(line_length) || line_length > k_max_reasonable_segment_mm) + continue; if (line_length < EPSILON) continue; - path_length += line_length; - auto dE = e_per_mm * line_length; - if (_needSAFC(path)) { - auto oldE = dE; - dE = m_small_area_infill_flow_compensator->modify_flow(line_length, dE, path.role()); - if (m_config.gcode_comments && oldE > 0 && oldE != dE) { - tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); + const bool dynamic_line_modulation = + outer_wall_gradient_dynamic_ctx.enabled && + (outer_wall_gradient_dynamic_ctx.object_center_mode || + outer_wall_gradient_dynamic_ctx.vertex_color_match_mode); + if (dynamic_line_modulation) { + 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) : + 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); + const int subsegment_count = std::clamp( + int(std::ceil(line.length() / std::max(modulation_step_scaled, EPSILON))), + 1, + 10000); + + Point sub_a = line.a; + for (int sub_idx = 1; sub_idx <= subsegment_count; ++sub_idx) { + std::string subDescription = tempDescription; + const double t = double(sub_idx) / double(subsegment_count); + const Point sub_b = (sub_idx == subsegment_count) ? + line.b : + Point(coord_t(std::llround(double(line.a.x()) + (double(line.b.x()) - double(line.a.x())) * t)), + coord_t(std::llround(double(line.a.y()) + (double(line.b.y()) - double(line.a.y())) * t))); + const Line sub_line(sub_a, sub_b); + const double sub_line_length = sub_line.length() * SCALING_FACTOR; + if (!std::isfinite(sub_line_length) || sub_line_length > k_max_reasonable_segment_mm) { + sub_a = sub_b; + continue; + } + if (sub_line_length < EPSILON) { + sub_a = sub_b; + continue; + } + + path_length += sub_line_length; + const OuterWallGradientSegmentMod sub_mod = dynamic_modulation_for_line(sub_line); + auto dE = e_per_mm * sub_line_length * sub_mod.flow_scale; + if (_needSAFC(path)) { + auto oldE = dE; + dE = m_small_area_infill_flow_compensator->modify_flow(sub_line_length, dE, path.role()); + if (m_config.gcode_comments && oldE > 0 && oldE != dE) { + subDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f", oldE, sub_line_length); + } + } + + const Point target_point = make_shifted_point(sub_line.b, sub_mod.shift_dx, sub_mod.shift_dy); + const Vec2d target_xy = this->point_to_gcode(target_point); + if (!is_reasonable_quantized_gcode_point_for_gcode(target_xy)) { + sub_a = sub_b; + continue; + } + if (!can_emit_extrusion_delta(dE)) { + sub_a = sub_b; + continue; + } + if (sloped == nullptr) { + gcode += m_writer.extrude_to_xy(target_xy, dE, + GCodeWriter::full_gcode_comment ? subDescription : "", + path.is_force_no_extrusion()); + } else { + if (!std::isfinite(total_length) || total_length <= EPSILON) { + sub_a = sub_b; + continue; + } + const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); + Vec3d dest3d(target_xy(0), target_xy(1), get_sloped_z(z_ratio)); + if (!can_emit_sloped_extrusion(dest3d, dE * e_ratio)) { + sub_a = sub_b; + continue; + } + gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, + GCodeWriter::full_gcode_comment ? subDescription : "", + path.is_force_no_extrusion()); + } + + emitted_last_point = target_point; + sub_a = sub_b; } - } - if (sloped == nullptr) { - // Normal extrusion - gcode += m_writer.extrude_to_xy( - this->point_to_gcode(line.b), - dE, - GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); } else { - // Sloped extrusion - const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); - Vec2d dest2d = this->point_to_gcode(line.b); - Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz( - dest3d, - dE * e_ratio, - GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + path_length += line_length; + + const OuterWallGradientSegmentMod segment_mod = segment_modulation_at(segment_idx); + auto dE = e_per_mm * line_length * segment_mod.flow_scale; + if (_needSAFC(path)) { + auto oldE = dE; + dE = m_small_area_infill_flow_compensator->modify_flow(line_length, dE, path.role()); + + if (m_config.gcode_comments && oldE > 0 && oldE != dE) { + tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); + } + } + + const Point target_point = make_shifted_point(line.b, segment_mod.shift_dx, segment_mod.shift_dy); + const Vec2d target_xy = this->point_to_gcode(target_point); + if (!is_reasonable_quantized_gcode_point_for_gcode(target_xy)) + continue; + if (!can_emit_extrusion_delta(dE)) + continue; + if (sloped == nullptr) { + gcode += m_writer.extrude_to_xy( + target_xy, + dE, + GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + } else { + if (!std::isfinite(total_length) || total_length <= EPSILON) + continue; + const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); + Vec3d dest3d(target_xy(0), target_xy(1), get_sloped_z(z_ratio)); + if (!can_emit_sloped_extrusion(dest3d, dE * e_ratio)) + continue; + gcode += m_writer.extrude_to_xyz( + dest3d, + dE * e_ratio, + GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + } + + emitted_last_point = target_point; } } } else { @@ -6866,6 +9064,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + emitted_last_point = line.b; } break; } @@ -6891,6 +9090,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE, arc.direction == ArcDirection::Arc_Dir_CCW, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + emitted_last_point = arc.end_point; break; } default: @@ -6914,7 +9114,19 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, total_length = l.length() * SCALING_FACTOR; } gcode += m_writer.set_speed(last_set_speed, "", comment); - Vec2d prev = this->point_to_gcode_quantized(new_points[0].p); + Point prev_point_model = new_points[0].p; + if (outer_wall_gradient_modulated_path && new_points.size() > 1) { + const OuterWallGradientSegmentMod first_mod = + (outer_wall_gradient_dynamic_ctx.enabled && + (outer_wall_gradient_dynamic_ctx.object_center_mode || + outer_wall_gradient_dynamic_ctx.vertex_color_match_mode)) ? + dynamic_modulation_for_line(Line(new_points[0].p, new_points[1].p)) : + segment_modulation_at(0); + prev_point_model = make_shifted_point(prev_point_model, first_mod.shift_dx, first_mod.shift_dy); + } + Vec2d prev = this->point_to_gcode_quantized(prev_point_model); + bool has_valid_prev = is_reasonable_quantized_gcode_point_for_gcode(prev); + emitted_last_point = has_valid_prev ? prev_point_model : path.first_point(); bool pre_fan_enabled = false; bool cur_fan_enabled = false; if( m_enable_cooling_markers && enable_overhang_bridge_fan) @@ -6928,7 +9140,18 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, std::string tempDescription = description; const ProcessedPoint &processed_point = new_points[i]; const ProcessedPoint &pre_processed_point = new_points[i-1]; - Vec2d p = this->point_to_gcode_quantized(processed_point.p); + const bool dynamic_line_modulation = + outer_wall_gradient_dynamic_ctx.enabled && + (outer_wall_gradient_dynamic_ctx.object_center_mode || + outer_wall_gradient_dynamic_ctx.vertex_color_match_mode); + const OuterWallGradientSegmentMod segment_mod = + dynamic_line_modulation ? + dynamic_modulation_for_line(Line(pre_processed_point.p, processed_point.p)) : + segment_modulation_at(i - 1); + const Point processed_target_point = make_shifted_point(processed_point.p, segment_mod.shift_dx, segment_mod.shift_dy); + Vec2d p = this->point_to_gcode_quantized(processed_target_point); + if (!is_reasonable_quantized_gcode_point_for_gcode(p)) + continue; if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { cur_fan_enabled = check_overhang_fan(processed_point.overlap, path.role()); @@ -6942,6 +9165,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, apply_role_based_fan_speed(); } + if (!has_valid_prev) { + prev = p; + emitted_last_point = processed_target_point; + has_valid_prev = true; + continue; + } + const double line_length = (p - prev).norm(); if(line_length < EPSILON) continue; @@ -7001,7 +9231,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, gcode += m_writer.set_speed(F, "", comment); last_set_speed = F; } - auto dE = e_per_mm * line_length; + auto dE = e_per_mm * line_length * segment_mod.flow_scale; if (_needSAFC(path)) { auto oldE = dE; dE = m_small_area_infill_flow_compensator->modify_flow(line_length, dE, path.role()); @@ -7010,6 +9240,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } + if (!can_emit_extrusion_delta(dE)) + continue; if (sloped == nullptr) { // Normal extrusion gcode += m_writer.extrude_to_xy(p, dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); @@ -7017,9 +9249,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); + if (!can_emit_sloped_extrusion(dest3d, dE * e_ratio)) + continue; gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } + emitted_last_point = processed_target_point; prev = p; } @@ -7032,7 +9267,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, m_last_notgapfill_extrusion_role = path.role(); } - this->set_last_pos(path.last_point()); + this->set_last_pos(emitted_last_point); return gcode; } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 1971415286..8dc1330cc2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -29,10 +29,15 @@ #include "GCode/TimelapsePosPicker.hpp" +#include +#include #include #include +#include #include #include +#include +#include #include namespace Slic3r { @@ -44,6 +49,33 @@ namespace CustomGCode{ struct Item; } struct PrintInstance; class ConstPrintObjectPtrsAdaptor; +struct VertexColorOverhangWeightField { + float min_x_mm { 0.f }; + float min_y_mm { 0.f }; + float bucket_width_mm { 1.f }; + float bucket_height_mm { 1.f }; + int bucket_width { 0 }; + int bucket_height { 0 }; + size_t component_count { 0 }; + std::vector sample_x_mm; + std::vector sample_y_mm; + std::vector sample_weight; + std::vector sample_component_weights; + std::vector> buckets; + std::vector fallback_weights; + + bool empty() const + { + return bucket_width <= 0 || + bucket_height <= 0 || + component_count == 0 || + sample_x_mm.empty() || + sample_y_mm.size() != sample_x_mm.size() || + sample_weight.size() != sample_x_mm.size() || + sample_component_weights.size() != sample_x_mm.size() * component_count; + } +}; + class OozePrevention { public: bool enable; @@ -490,6 +522,8 @@ private: ExtrusionQualityEstimator m_extrusion_quality_estimator; + std::map, VertexColorOverhangWeightField> m_vertex_color_overhang_weight_field_cache; + bool m_warned_texture_mapping_filament_count_mismatch { false }; /* Origin of print coordinates expressed in unscaled G-code coordinates. This affects the input arguments supplied to the extrude*() and travel_to() @@ -641,6 +675,7 @@ private: double calc_max_volumetric_speed(const double layer_height, const double line_width, const std::string co_str); std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1); + std::optional texture_mapping_seam_hiding_point(const ExtrusionLoop &loop); bool _needSAFC(const ExtrusionPath &path); void print_machine_envelope(GCodeOutputStream& file, Print& print); void _print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 1dd7cc8da2..e81d49379c 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -7,6 +7,7 @@ #include "GCode/ToolOrderUtils.hpp" #include "FilamentGroupUtils.hpp" #include "I18N.hpp" +#include "../TextureMapping.hpp" // #define SLIC3R_DEBUG @@ -83,19 +84,25 @@ bool check_filament_printable_after_group(const std::vector &used_ unsigned int LayerTools::wall_filament(const PrintRegion ®ion) const { assert(region.config().wall_filament.value > 0); - return ((this->extruder_override == 0) ? region.config().wall_filament.value : this->extruder_override) - 1; + const unsigned int filament_id = (this->extruder_override == 0) ? region.config().wall_filament.value : this->extruder_override; + const unsigned int resolved = this->resolve_filament_id(filament_id); + return resolved > 0 ? resolved - 1 : 0; } unsigned int LayerTools::sparse_infill_filament(const PrintRegion ®ion) const { assert(region.config().sparse_infill_filament.value > 0); - return ((this->extruder_override == 0) ? region.config().sparse_infill_filament.value : this->extruder_override) - 1; + const unsigned int filament_id = (this->extruder_override == 0) ? region.config().sparse_infill_filament.value : this->extruder_override; + const unsigned int resolved = this->resolve_filament_id(filament_id); + return resolved > 0 ? resolved - 1 : 0; } unsigned int LayerTools::solid_infill_filament(const PrintRegion ®ion) const { assert(region.config().solid_infill_filament.value > 0); - return ((this->extruder_override == 0) ? region.config().solid_infill_filament.value : this->extruder_override) - 1; + const unsigned int filament_id = (this->extruder_override == 0) ? region.config().solid_infill_filament.value : this->extruder_override; + const unsigned int resolved = this->resolve_filament_id(filament_id); + return resolved > 0 ? resolved - 1 : 0; } // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden. @@ -117,7 +124,19 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c } else extruder = this->extruder_override; - return (extruder == 0) ? 0 : extruder - 1; + const unsigned int resolved = this->resolve_filament_id(extruder); + return (resolved == 0) ? 0 : resolved - 1; +} + +unsigned int LayerTools::resolve_filament_id(unsigned int filament_id_1based) const +{ + if (filament_id_1based == 0) + return 0; + if (texture_mapping_manager != nullptr && + num_physical_filaments > 0 && + texture_mapping_manager->is_texture_mapping_zone_id(filament_id_1based)) + return texture_mapping_manager->resolve_zone_component(filament_id_1based, num_physical_filaments, layer_index); + return filament_id_1based; } static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height) @@ -527,7 +546,12 @@ std::vector ToolOrdering::generate_first_layer_tool_order(const Pr return tool_order; for (auto layerm : target_layer->regions()) { - int extruder_id = layerm->region().config().option("wall_filament")->getInt(); + const int raw_extruder_id = layerm->region().config().option("wall_filament")->getInt(); + const unsigned int resolved_extruder_id = raw_extruder_id <= 0 ? 0 : + print.texture_mapping_manager().resolve_zone_component(unsigned(raw_extruder_id), print.config().filament_colour.size(), int(target_layer->id())); + if (resolved_extruder_id == 0 || resolved_extruder_id > print.config().filament_colour.size()) + continue; + int extruder_id = int(resolved_extruder_id); for (auto expoly : layerm->raw_slices) { const double nozzle_diameter = print.config().nozzle_diameter.get_at(0); @@ -591,7 +615,12 @@ std::vector ToolOrdering::generate_first_layer_tool_order(const Pr return tool_order; for (auto layerm : target_layer->regions()) { - int extruder_id = layerm->region().config().option("wall_filament")->getInt(); + const int raw_extruder_id = layerm->region().config().option("wall_filament")->getInt(); + const unsigned int resolved_extruder_id = raw_extruder_id <= 0 ? 0 : + object.print()->texture_mapping_manager().resolve_zone_component(unsigned(raw_extruder_id), object.print()->config().filament_colour.size(), int(target_layer->id())); + if (resolved_extruder_id == 0 || resolved_extruder_id > object.print()->config().filament_colour.size()) + continue; + int extruder_id = int(resolved_extruder_id); for (auto expoly : layerm->raw_slices) { const double nozzle_diameter = object.print()->config().nozzle_diameter.get_at(0); const coordf_t line_width = object.config().get_abs_value("line_width", nozzle_diameter); @@ -639,6 +668,12 @@ void ToolOrdering::initialize_layers(std::vector &zs) for (; j < zs.size() && zs[j] <= zmax; ++ j) ; // Assign an average print_z to the set of layers with nearly equal print_z. m_layer_tools.emplace_back(LayerTools(0.5 * (zs[i] + zs[j-1]))); + LayerTools &layer_tools = m_layer_tools.back(); + layer_tools.layer_index = int(m_layer_tools.size() - 1); + if (m_print != nullptr) { + layer_tools.texture_mapping_manager = &m_print->texture_mapping_manager(); + layer_tools.num_physical_filaments = m_print->config().filament_colour.size(); + } i = j; } } @@ -682,9 +717,10 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } if (something_nonoverriddable){ - layer_tools.extruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override); + const unsigned int filament_id = (extruder_override == 0) ? region.config().wall_filament.value : extruder_override; + layer_tools.extruders.emplace_back(layer_tools.resolve_filament_id(filament_id)); if (layerCount == 0) { - firstLayerExtruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override); + firstLayerExtruders.emplace_back(layer_tools.resolve_filament_id(filament_id)); } } @@ -710,13 +746,13 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } if (something_nonoverriddable || !m_print_config_ptr) { - if (extruder_override == 0) { + if (extruder_override == 0) { if (has_solid_infill) - layer_tools.extruders.emplace_back(region.config().solid_infill_filament); + layer_tools.extruders.emplace_back(layer_tools.resolve_filament_id(region.config().solid_infill_filament)); if (has_infill) - layer_tools.extruders.emplace_back(region.config().sparse_infill_filament); - } else if (has_solid_infill || has_infill) - layer_tools.extruders.emplace_back(extruder_override); + layer_tools.extruders.emplace_back(layer_tools.resolve_filament_id(region.config().sparse_infill_filament)); + } else if (has_solid_infill || has_infill) + layer_tools.extruders.emplace_back(layer_tools.resolve_filament_id(extruder_override)); } if (has_solid_infill || has_infill) layer_tools.has_object = true; @@ -741,6 +777,8 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } unsigned int extruder_support = object.config().support_filament.value; unsigned int extruder_interface = object.config().support_interface_filament.value; + extruder_support = layer_tools.resolve_filament_id(extruder_support); + extruder_interface = layer_tools.resolve_filament_id(extruder_interface); if (has_support) { if (extruder_support > 0 || !has_interface || extruder_interface == 0 || layer_tools.has_object) layer_tools.extruders.push_back(extruder_support); diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f584b20707..edf712728d 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -19,6 +19,7 @@ class PrintObject; class LayerTools; namespace CustomGCode { struct Item; } class PrintRegion; +class TextureMappingManager; // Object of this class holds information about whether an extrusion is printed immediately // after a toolchange (as part of infill/perimeter wiping) or not. One extrusion can be a part @@ -144,8 +145,12 @@ public: unsigned int solid_infill_filament(const PrintRegion ®ion) const; // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden. unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion ®ion) const; + unsigned int resolve_filament_id(unsigned int filament_id_1based) const; coordf_t print_z = 0.; + int layer_index = 0; + size_t num_physical_filaments = 0; + const TextureMappingManager *texture_mapping_manager = nullptr; bool has_object = false; bool has_support = false; // Zero based extruder IDs, ordered to minimize tool switches. diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index e3b2ada837..5a43ded8b5 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -11,6 +11,7 @@ #include "TriangleMeshSlicer.hpp" #include "TriangleSelector.hpp" #include "MaterialType.hpp" +#include "PNGReadWrite.hpp" #include "Format/AMF.hpp" #include "Format/svg.hpp" @@ -22,13 +23,22 @@ #include "libslic3r/Geometry/ConvexHull.hpp" #include +#include +#include +#include +#include +#include +#include #include #include #include #include +#include #include +#include + #include "SVG.hpp" #include #include @@ -56,6 +66,470 @@ const std::vector CONST_FILAMENTS = { // BBS initialization of static variables std::map Model::extruderParamsMap = { {0,{"",0,0}}}; GlobalSpeedMap Model::printSpeedMap{}; + +namespace { + +static bool checked_rgba_buffer_size(size_t width, size_t height, size_t &buffer_size) +{ + buffer_size = 0; + if (width == 0 || height == 0) + return false; + if (width > std::numeric_limits::max() / height) + return false; + const size_t pixel_count = width * height; + if (pixel_count > std::numeric_limits::max() / 4) + return false; + buffer_size = pixel_count * 4; + return true; +} + +static std::vector split_obj_mtl_tokens(const std::string &line) +{ + std::vector tokens; + std::string current; + char quote_char = '\0'; + for (const char c : line) { + if ((c == '"' || c == '\'') && (quote_char == '\0' || quote_char == c)) { + quote_char = quote_char == '\0' ? c : '\0'; + continue; + } + if (quote_char == '\0' && std::isspace(static_cast(c))) { + if (!current.empty()) { + tokens.emplace_back(std::move(current)); + current.clear(); + } + continue; + } + current.push_back(c); + } + if (!current.empty()) + tokens.emplace_back(std::move(current)); + return tokens; +} + +static std::string extract_obj_texture_reference(const std::string &map_kd_value) +{ + const std::vector tokens = split_obj_mtl_tokens(map_kd_value); + if (tokens.empty()) + return {}; + return tokens.back(); +} + +static std::vector resolve_obj_texture_path_candidates(const std::string &obj_path, const std::string &texture_reference) +{ + std::vector candidates; + if (texture_reference.empty()) + return candidates; + + auto push_unique = [&candidates](const boost::filesystem::path &path) { + if (path.empty()) + return; + const std::string normalized = path.lexically_normal().string(); + if (normalized.empty()) + return; + if (std::find(candidates.begin(), candidates.end(), normalized) == candidates.end()) + candidates.emplace_back(normalized); + }; + + const boost::filesystem::path texture_path(texture_reference); + const boost::filesystem::path obj_dir = boost::filesystem::path(obj_path).parent_path(); + + if (texture_path.is_absolute()) { + push_unique(texture_path); + if (!texture_path.filename().empty()) + push_unique(obj_dir / texture_path.filename()); + } else { + push_unique(obj_dir / texture_path); + if (!texture_path.filename().empty()) + push_unique(obj_dir / texture_path.filename()); + } + + return candidates; +} + +static bool decode_png_texture_rgba(const std::string &texture_path, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + boost::nowide::ifstream ifs(texture_path, std::ios::binary); + if (!ifs.is_open()) + return false; + + std::string encoded_data((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + if (encoded_data.empty()) + return false; + + png::ReadBuf rb{encoded_data.data(), encoded_data.size()}; + png::ImageColorscale img; + if (!png::decode_colored_png(rb, img)) + return false; + + if (img.cols == 0 || img.rows == 0 || (img.bytes_per_pixel != 3 && img.bytes_per_pixel != 4)) + return false; + + size_t rgba_size = 0; + if (!checked_rgba_buffer_size(img.cols, img.rows, rgba_size)) + return false; + + const size_t row_stride = img.cols * size_t(img.bytes_per_pixel); + if (img.buf.size() < img.rows * row_stride) + return false; + + out_rgba.assign(rgba_size, 255); + for (size_t y = 0; y < img.rows; ++y) { + const size_t src_row_off = y * row_stride; + const size_t dst_row_off = y * img.cols * 4; + for (size_t x = 0; x < img.cols; ++x) { + const size_t src = src_row_off + x * size_t(img.bytes_per_pixel); + const size_t dst = dst_row_off + x * 4; + out_rgba[dst + 0] = img.buf[src + 0]; + out_rgba[dst + 1] = img.buf[src + 1]; + out_rgba[dst + 2] = img.buf[src + 2]; + out_rgba[dst + 3] = (img.bytes_per_pixel == 4) ? img.buf[src + 3] : uint8_t(255); + } + } + + out_width = uint32_t(img.cols); + out_height = uint32_t(img.rows); + return true; +} + +struct JpegDecodeErrorManager +{ + jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +static void jpeg_decode_error_exit(j_common_ptr cinfo) +{ + auto *err = reinterpret_cast(cinfo->err); + longjmp(err->setjmp_buffer, 1); +} + +static bool decode_jpeg_texture_rgba(const std::string &texture_path, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + boost::nowide::ifstream ifs(texture_path, std::ios::binary); + if (!ifs.is_open()) + return false; + + std::string encoded_data((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + if (encoded_data.empty()) + return false; + + jpeg_decompress_struct cinfo{}; + JpegDecodeErrorManager jerr{}; + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = jpeg_decode_error_exit; + bool jpeg_created = false; + auto destroy_jpeg = [&cinfo, &jpeg_created]() { + if (jpeg_created) { + jpeg_destroy_decompress(&cinfo); + jpeg_created = false; + } + }; + + if (setjmp(jerr.setjmp_buffer)) { + destroy_jpeg(); + return false; + } + + jpeg_create_decompress(&cinfo); + jpeg_created = true; + jpeg_mem_src(&cinfo, + reinterpret_cast(encoded_data.data()), + static_cast(encoded_data.size())); + + if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { + destroy_jpeg(); + return false; + } + + if (!jpeg_start_decompress(&cinfo)) { + destroy_jpeg(); + return false; + } + + const uint32_t width = cinfo.output_width; + const uint32_t height = cinfo.output_height; + const int components = cinfo.output_components; + size_t rgba_size = 0; + const size_t scanline_stride = size_t(width) * size_t(std::max(components, 0)); + if (!checked_rgba_buffer_size(width, height, rgba_size) || + components <= 0 || + scanline_stride > std::numeric_limits::max()) { + jpeg_finish_decompress(&cinfo); + destroy_jpeg(); + return false; + } + + out_rgba.assign(rgba_size, uint8_t(255)); + JSAMPARRAY scanline = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, + JPOOL_IMAGE, + JDIMENSION(scanline_stride), + 1); + + uint32_t y = 0; + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, scanline, 1); + const unsigned char *src = scanline[0]; + for (uint32_t x = 0; x < width; ++x) { + const size_t dst = (size_t(y) * size_t(width) + size_t(x)) * 4; + if (components >= 3) { + const size_t s = size_t(x) * size_t(components); + out_rgba[dst + 0] = src[s + 0]; + out_rgba[dst + 1] = src[s + 1]; + out_rgba[dst + 2] = src[s + 2]; + } else { + const unsigned char g = src[x]; + out_rgba[dst + 0] = g; + out_rgba[dst + 1] = g; + out_rgba[dst + 2] = g; + } + out_rgba[dst + 3] = 255; + } + ++y; + } + + if (!jpeg_finish_decompress(&cinfo)) { + destroy_jpeg(); + return false; + } + destroy_jpeg(); + + out_width = width; + out_height = height; + return true; +} + +static bool decode_image_texture_rgba(const std::string &texture_path, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (boost::algorithm::iends_with(texture_path, ".png")) + return decode_png_texture_rgba(texture_path, out_rgba, out_width, out_height); + + if (boost::algorithm::iends_with(texture_path, ".jpg") || boost::algorithm::iends_with(texture_path, ".jpeg")) + return decode_jpeg_texture_rgba(texture_path, out_rgba, out_width, out_height); + + return false; +} + +struct ObjTextureImage +{ + std::string resolved_path; + std::vector rgba; + uint32_t width{0}; + uint32_t height{0}; +}; + +struct ObjTextureImportData +{ + std::vector textures; + std::unordered_map map_kd_to_texture_idx; +}; + +static ObjTextureImportData load_obj_albedo_textures(const std::string &obj_path, const ObjInfo &obj_info) +{ + ObjTextureImportData result; + std::unordered_map loaded_texture_path_to_idx; + + auto register_map = [&](const std::string &map_kd_raw) { + if (map_kd_raw.empty()) + return; + if (result.map_kd_to_texture_idx.find(map_kd_raw) != result.map_kd_to_texture_idx.end()) + return; + + const std::string texture_ref = extract_obj_texture_reference(map_kd_raw); + const auto candidates = resolve_obj_texture_path_candidates(obj_path, texture_ref); + + bool had_decode_failure = false; + std::string last_failed_path; + for (const std::string &candidate : candidates) { + if (!boost::filesystem::exists(candidate)) + continue; + + if (const auto existing = loaded_texture_path_to_idx.find(candidate); + existing != loaded_texture_path_to_idx.end()) { + result.map_kd_to_texture_idx[map_kd_raw] = existing->second; + return; + } + + ObjTextureImage image; + image.resolved_path = candidate; + if (!decode_image_texture_rgba(candidate, image.rgba, image.width, image.height)) { + had_decode_failure = true; + last_failed_path = candidate; + continue; + } + + const size_t texture_idx = result.textures.size(); + loaded_texture_path_to_idx[candidate] = texture_idx; + result.textures.emplace_back(std::move(image)); + result.map_kd_to_texture_idx[map_kd_raw] = texture_idx; + return; + } + + if (had_decode_failure) { + BOOST_LOG_TRIVIAL(error) << "OBJ albedo texture map found but failed to decode image" + << " map_Kd='" << map_kd_raw + << "' last_path='" << last_failed_path + << "' (supported: PNG, JPEG)."; + } else if (!candidates.empty()) { + BOOST_LOG_TRIVIAL(error) << "OBJ albedo texture map file not found" + << " map_Kd='" << map_kd_raw + << "' (checked OBJ-relative and filename fallback paths)."; + } else { + BOOST_LOG_TRIVIAL(error) << "OBJ albedo texture map reference is empty or invalid" + << " map_Kd='" << map_kd_raw << "'."; + } + }; + + for (const auto &face_to_map : obj_info.uv_map_pngs) + register_map(face_to_map.second); + + if (result.textures.empty() && !obj_info.single_texture_image.empty()) + register_map(obj_info.single_texture_image); + + return result; +} + +struct ObjTextureAtlasEntry +{ + uint32_t x_offset{0}; + uint32_t width{0}; + uint32_t height{0}; +}; + +static bool build_obj_texture_atlas(const ObjInfo &obj_info, + const ObjTextureImportData &texture_data, + std::vector> &triangle_uvs, + std::vector &triangle_uv_valid, + std::vector &atlas_rgba, + uint32_t &atlas_width, + uint32_t &atlas_height) +{ + atlas_rgba.clear(); + atlas_width = 0; + atlas_height = 0; + + if (texture_data.textures.empty()) + return false; + if (triangle_uvs.size() != obj_info.triangle_uvs.size() || triangle_uv_valid.size() != obj_info.triangle_uvs_valid.size()) + return false; + + std::vector placements(texture_data.textures.size()); + for (size_t i = 0; i < texture_data.textures.size(); ++i) { + const ObjTextureImage &texture = texture_data.textures[i]; + if (texture.width == 0 || texture.height == 0 || texture.rgba.empty()) + return false; + size_t texture_rgba_size = 0; + if (!checked_rgba_buffer_size(texture.width, texture.height, texture_rgba_size) || + texture.rgba.size() < texture_rgba_size || + texture.width > std::numeric_limits::max() - atlas_width) + return false; + + placements[i].x_offset = atlas_width; + placements[i].width = texture.width; + placements[i].height = texture.height; + atlas_width += texture.width; + atlas_height = std::max(atlas_height, texture.height); + } + + if (atlas_width == 0 || atlas_height == 0) + return false; + + size_t atlas_rgba_size = 0; + if (!checked_rgba_buffer_size(atlas_width, atlas_height, atlas_rgba_size)) + return false; + + atlas_rgba.assign(atlas_rgba_size, uint8_t(0)); + for (size_t i = 0; i < texture_data.textures.size(); ++i) { + const ObjTextureImage &texture = texture_data.textures[i]; + const ObjTextureAtlasEntry &entry = placements[i]; + for (uint32_t y = 0; y < texture.height; ++y) { + const size_t src_off = size_t(y) * size_t(texture.width) * 4; + const size_t dst_off = (size_t(y) * size_t(atlas_width) + size_t(entry.x_offset)) * 4; + std::copy(texture.rgba.begin() + src_off, + texture.rgba.begin() + src_off + size_t(texture.width) * 4, + atlas_rgba.begin() + dst_off); + } + } + + auto wrap_uv = [](float value) { + if (!std::isfinite(value)) + return 0.f; + + constexpr float k_uv_epsilon = 1e-6f; + if (value >= -k_uv_epsilon && value <= 1.f + k_uv_epsilon) + return std::clamp(value, 0.f, 1.f); + + const float wrapped = value - std::floor(value); + return wrapped < 0.f ? wrapped + 1.f : wrapped; + }; + + auto remap_uv = [&wrap_uv, &atlas_width, &atlas_height](const Vec2f &uv, const ObjTextureAtlasEntry &entry) { + const float u = wrap_uv(uv.x()); + const float v = wrap_uv(uv.y()); + return Vec2f((float(entry.x_offset) + u * float(entry.width)) / float(atlas_width), + v * float(entry.height) / float(atlas_height)); + }; + + bool has_any_textured_triangle = false; + for (size_t tri_idx = 0; tri_idx < triangle_uvs.size(); ++tri_idx) { + triangle_uv_valid[tri_idx] = 0; + if (obj_info.triangle_uvs_valid[tri_idx] == 0) + continue; + + size_t texture_idx = size_t(-1); + const auto face_to_texture = obj_info.uv_map_pngs.find(int(tri_idx)); + if (face_to_texture != obj_info.uv_map_pngs.end() && !face_to_texture->second.empty()) { + const auto texture_it = texture_data.map_kd_to_texture_idx.find(face_to_texture->second); + if (texture_it != texture_data.map_kd_to_texture_idx.end()) + texture_idx = texture_it->second; + } + + if (texture_idx == size_t(-1) && texture_data.textures.size() == 1) + texture_idx = 0; + if (texture_idx == size_t(-1)) + continue; + + const ObjTextureAtlasEntry &entry = placements[texture_idx]; + triangle_uvs[tri_idx][0] = remap_uv(triangle_uvs[tri_idx][0], entry); + triangle_uvs[tri_idx][1] = remap_uv(triangle_uvs[tri_idx][1], entry); + triangle_uvs[tri_idx][2] = remap_uv(triangle_uvs[tri_idx][2], entry); + triangle_uv_valid[tri_idx] = 1; + has_any_textured_triangle = true; + } + + if (!has_any_textured_triangle) { + atlas_rgba.clear(); + atlas_width = 0; + atlas_height = 0; + return false; + } + + return true; +} + +} + Model& Model::assign_copy(const Model &rhs) { this->copy_id(rhs); @@ -251,7 +725,8 @@ Model Model::read_from_file(const std::string& ImportstlProgressFn stlFn, BBLProject * project, int plate_id, - ObjImportColorFn objFn) + ObjImportColorFn objFn, + ObjImportModeFn objModeFn) { Model model; @@ -283,30 +758,115 @@ Model Model::read_from_file(const std::string& ObjInfo obj_info; result = load_obj(input_file.c_str(), &model, obj_info, message); if (result){ - ObjDialogInOut in_out; - in_out.model = &model; - in_out.lost_material_name = obj_info.lost_material_name; - if (obj_info.vertex_colors.size() > 0) { - if (objFn) { // 1.result is ok and pop up a dialog - in_out.input_colors = std::move(obj_info.vertex_colors); + if (!message.empty()) + BOOST_LOG_TRIVIAL(error) << message; + + const ObjTextureImportData texture_import_data = load_obj_albedo_textures(input_file, obj_info); + const bool has_valid_texture_uvs = std::any_of(obj_info.triangle_uvs_valid.begin(), obj_info.triangle_uvs_valid.end(), [](uint8_t uv_valid) { + return uv_valid != 0; + }); + const ObjImportCapabilities capabilities{ + !obj_info.vertex_colors.empty(), + !obj_info.face_colors.empty(), + obj_info.is_single_mtl, + texture_import_data.textures.size(), + has_valid_texture_uvs + }; + const bool has_mode_selection = bool(objModeFn); + ObjImportMode import_mode = ObjImportMode::UseDefault; + const bool has_usable_uv_texture_data = capabilities.texture_count > 0 && capabilities.has_valid_texture_uvs; + if (has_mode_selection) { + import_mode = objModeFn(capabilities); + if (import_mode == ObjImportMode::UseDefault) + import_mode = has_usable_uv_texture_data ? ObjImportMode::ImportTextures : ObjImportMode::ImportPaintedRegions; + } + + if (model.objects.size() == 1) { + ModelObject *obj = model.objects.front(); + if (obj != nullptr && obj->volumes.size() == 1 && obj->volumes.front() != nullptr) { + ModelVolume *volume = obj->volumes.front(); + + volume->imported_vertex_colors_rgba.clear(); + volume->imported_texture_uvs_per_face.clear(); + volume->imported_texture_uv_valid.clear(); + volume->imported_texture_rgba.clear(); + volume->imported_texture_width = 0; + volume->imported_texture_height = 0; + bool has_imported_usable_uv_texture_data = false; + + const size_t triangle_count = volume->mesh().its.indices.size(); + if (triangle_count == obj_info.triangle_uvs.size() && triangle_count == obj_info.triangle_uvs_valid.size()) { + std::vector> triangle_uvs = obj_info.triangle_uvs; + std::vector triangle_uv_valid = obj_info.triangle_uvs_valid; + + const bool import_textures = has_mode_selection ? + (import_mode == ObjImportMode::ImportTextures) : + true; + + if (import_textures) { + std::vector atlas_rgba; + uint32_t atlas_width = 0; + uint32_t atlas_height = 0; + if (build_obj_texture_atlas(obj_info, texture_import_data, triangle_uvs, triangle_uv_valid, atlas_rgba, atlas_width, atlas_height)) { + volume->imported_texture_uvs_per_face.reserve(triangle_count * 6); + volume->imported_texture_uv_valid.reserve(triangle_count); + bool has_any_valid_uv_face = false; + for (size_t face_idx = 0; face_idx < triangle_count; ++face_idx) { + const std::array &uv = triangle_uvs[face_idx]; + volume->imported_texture_uvs_per_face.emplace_back(uv[0].x()); + volume->imported_texture_uvs_per_face.emplace_back(uv[0].y()); + volume->imported_texture_uvs_per_face.emplace_back(uv[1].x()); + volume->imported_texture_uvs_per_face.emplace_back(uv[1].y()); + volume->imported_texture_uvs_per_face.emplace_back(uv[2].x()); + volume->imported_texture_uvs_per_face.emplace_back(uv[2].y()); + volume->imported_texture_uv_valid.emplace_back(triangle_uv_valid[face_idx]); + has_any_valid_uv_face = has_any_valid_uv_face || (triangle_uv_valid[face_idx] != 0); + } + volume->imported_texture_width = atlas_width; + volume->imported_texture_height = atlas_height; + volume->imported_texture_rgba = std::move(atlas_rgba); + has_imported_usable_uv_texture_data = has_any_valid_uv_face; + } + } + } + + const bool import_vertex_colors = has_mode_selection ? + (import_mode == ObjImportMode::ImportPaintedRegions || + (import_mode == ObjImportMode::ImportTextures && !has_imported_usable_uv_texture_data)) : + true; + if (import_vertex_colors && volume->mesh().its.vertices.size() == obj_info.vertex_colors.size()) { + volume->imported_vertex_colors_rgba.reserve(obj_info.vertex_colors.size()); + for (const RGBA &color : obj_info.vertex_colors) { + const uint32_t r = uint32_t(std::lround(std::clamp(color[0], 0.f, 1.f) * 255.f)) & 0xFFu; + const uint32_t g = uint32_t(std::lround(std::clamp(color[1], 0.f, 1.f) * 255.f)) & 0xFFu; + const uint32_t b = uint32_t(std::lround(std::clamp(color[2], 0.f, 1.f) * 255.f)) & 0xFFu; + const uint32_t a = uint32_t(std::lround(std::clamp(color[3], 0.f, 1.f) * 255.f)) & 0xFFu; + volume->imported_vertex_colors_rgba.emplace_back((r << 24) | (g << 16) | (b << 8) | a); + } + } + } + } + + const bool import_painted_regions = has_mode_selection ? + (import_mode == ObjImportMode::ImportPaintedRegions) : + true; + + if (import_painted_regions && objFn) { + ObjDialogInOut in_out; + in_out.model = &model; + in_out.lost_material_name = obj_info.lost_material_name; + if (obj_info.vertex_colors.size() > 0) { + in_out.input_colors = obj_info.vertex_colors; in_out.is_single_color = false; in_out.deal_vertex_color = true; objFn(in_out); - } - } else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { // mtl file - if (objFn) { // 1.result is ok and pop up a dialog - in_out.input_colors = std::move(obj_info.face_colors); + } else if (obj_info.face_colors.size() > 0) { + in_out.input_colors = obj_info.face_colors; in_out.is_single_color = obj_info.is_single_mtl; in_out.deal_vertex_color = false; objFn(in_out); } - } /*else if (obj_info.has_uv_png && obj_info.uvs.size() > 0) { - boost::filesystem::path full_path(input_file); - std::string obj_directory = full_path.parent_path().string(); - obj_info.obj_dircetory = obj_directory; - result = false; - message = _L("Importing obj with png function is developing."); - }*/ + } } } else if (boost::algorithm::iends_with(input_file, ".svg")) @@ -2999,122 +3559,133 @@ static void get_real_filament_id(const unsigned char &id, std::string &result) { bool Model::obj_import_vertex_color_deal(const std::vector &vertex_filament_ids, const unsigned char &first_extruder_id, Model *model) { - if (vertex_filament_ids.size() == 0) { + if (model == nullptr || model->objects.size() != 1) { return false; } - // 2.generate mmu_segmentation_facets - if (model->objects.size() == 1 ) { - auto obj = model->objects[0]; - obj->config.set("extruder", first_extruder_id); - if (obj->volumes.size() == 1) { - enum VertexColorCase { - _3_SAME_COLOR, - _3_DIFF_COLOR, - _2_SAME_1_DIFF_COLOR, - }; - auto calc_vertex_color_case = [](const unsigned char &c0, const unsigned char &c1, const unsigned char &c2, VertexColorCase &vertex_color_case, - unsigned char &iso_index) { - if (c0 == c1 && c1 == c2) { - vertex_color_case = VertexColorCase::_3_SAME_COLOR; - } else if (c0 != c1 && c1 != c2 && c0 != c2) { - vertex_color_case = VertexColorCase::_3_DIFF_COLOR; - } else if (c0 == c1) { - vertex_color_case = _2_SAME_1_DIFF_COLOR; - iso_index = 2; - } else if (c1 == c2) { - vertex_color_case = _2_SAME_1_DIFF_COLOR; - iso_index = 0; - } else if (c0 == c2) { - vertex_color_case = _2_SAME_1_DIFF_COLOR; - iso_index = 1; - } else { - std::cout << "error"; - } - }; - auto calc_tri_area = [](const Vec3f &v0, const Vec3f &v1, const Vec3f &v2) { - return std::abs((v0 - v1).cross(v0 - v2).norm()) / 2; - }; - auto volume = obj->volumes[0]; - volume->config.set("extruder", first_extruder_id); - auto face_count = volume->mesh().its.indices.size(); - volume->mmu_segmentation_facets.reset(); - volume->mmu_segmentation_facets.reserve(face_count); - if (volume->mesh().its.vertices.size() != vertex_filament_ids.size()) { - return false; - } - for (size_t i = 0; i < volume->mesh().its.indices.size(); i++) { - auto face = volume->mesh().its.indices[i]; - auto filament_id0 = vertex_filament_ids[face[0]]; - auto filament_id1 = vertex_filament_ids[face[1]]; - auto filament_id2 = vertex_filament_ids[face[2]]; - if (filament_id0 <= 1 && filament_id1 <= 1 && filament_id2 <= 2) { - continue; - } - VertexColorCase vertex_color_case; - unsigned char iso_index; - calc_vertex_color_case(filament_id0, filament_id1, filament_id2, vertex_color_case, iso_index); - switch (vertex_color_case) { - case _3_SAME_COLOR: { - std::string result; - get_real_filament_id(filament_id0, result); - volume->mmu_segmentation_facets.set_triangle_from_string(i, result); - break; - } - case _3_DIFF_COLOR: { - std::string result0, result1, result2; - get_real_filament_id(filament_id0, result0); - get_real_filament_id(filament_id1, result1); - get_real_filament_id(filament_id2, result2); + return obj_import_vertex_color_deal_for_object(vertex_filament_ids, first_extruder_id, model->objects[0]); +} - auto v0 = volume->mesh().its.vertices[face[0]]; - auto v1 = volume->mesh().its.vertices[face[1]]; - auto v2 = volume->mesh().its.vertices[face[2]]; - auto dir_0_1 = (v1 - v0).normalized().eval(); - auto dir_0_2 = (v2 - v0).normalized().eval(); - float sita0 = acos(dir_0_1.dot(dir_0_2)); - auto dir_1_0 = (-dir_0_1).eval(); - auto dir_1_2 = (v2 - v1).normalized().eval(); - float sita1 = acos(dir_1_0.dot(dir_1_2)); - float sita2 = PI - sita0 - sita1; - std::array sitas = {sita0, sita1, sita2}; - float max_sita = sitas[0]; - int max_sita_vertex_index = 0; - for (size_t j = 1; j < sitas.size(); j++) { - if (sitas[j] > max_sita) { - max_sita_vertex_index = j; - max_sita = sitas[j]; - } - } - if (max_sita_vertex_index == 0) { - volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result2 + (result1 + result2 + "5" )+ "3"); //"1C0C2C0C1C13" - } else if (max_sita_vertex_index == 1) { - volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result2 + (result0 + result2 + "9") + "3"); - } else{// if (max_sita_vertex_index == 2) - volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result2 + (result1 + result0 + "1") + "3"); - } - break; - } - case _2_SAME_1_DIFF_COLOR: { - std::string result0, result1, result2; - get_real_filament_id(filament_id0, result0); - get_real_filament_id(filament_id1, result1); - get_real_filament_id(filament_id2, result2); - if (iso_index == 0) { - volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result1 + "2"); - } else if (iso_index == 1) { - volume->mmu_segmentation_facets.set_triangle_from_string(i, result1 + result0 + result0 + "6"); - } else if (iso_index == 2) { - volume->mmu_segmentation_facets.set_triangle_from_string(i, result2 + result0 + result0 + "A"); - } - break; - } - default: break; +bool Model::obj_import_vertex_color_deal_for_object(const std::vector &vertex_filament_ids, + const unsigned char &first_extruder_id, + ModelObject *object) +{ + if (vertex_filament_ids.size() == 0 || object == nullptr) { + return false; + } + + auto obj = object; + obj->config.set("extruder", first_extruder_id); + if (obj->volumes.size() != 1) + return false; + + auto volume = obj->volumes[0]; + if (volume == nullptr) + return false; + + enum VertexColorCase { + _3_SAME_COLOR, + _3_DIFF_COLOR, + _2_SAME_1_DIFF_COLOR, + }; + auto calc_vertex_color_case = [](const unsigned char &c0, const unsigned char &c1, const unsigned char &c2, VertexColorCase &vertex_color_case, + unsigned char &iso_index) { + if (c0 == c1 && c1 == c2) { + vertex_color_case = VertexColorCase::_3_SAME_COLOR; + } else if (c0 != c1 && c1 != c2 && c0 != c2) { + vertex_color_case = VertexColorCase::_3_DIFF_COLOR; + } else if (c0 == c1) { + vertex_color_case = _2_SAME_1_DIFF_COLOR; + iso_index = 2; + } else if (c1 == c2) { + vertex_color_case = _2_SAME_1_DIFF_COLOR; + iso_index = 0; + } else if (c0 == c2) { + vertex_color_case = _2_SAME_1_DIFF_COLOR; + iso_index = 1; + } else { + std::cout << "error"; + } + }; + auto calc_tri_area = [](const Vec3f &v0, const Vec3f &v1, const Vec3f &v2) { + return std::abs((v0 - v1).cross(v0 - v2).norm()) / 2; + }; + volume->config.set("extruder", first_extruder_id); + auto face_count = volume->mesh().its.indices.size(); + volume->mmu_segmentation_facets.reset(); + volume->mmu_segmentation_facets.reserve(face_count); + if (volume->mesh().its.vertices.size() != vertex_filament_ids.size()) { + return false; + } + for (size_t i = 0; i < volume->mesh().its.indices.size(); i++) { + auto face = volume->mesh().its.indices[i]; + auto filament_id0 = vertex_filament_ids[face[0]]; + auto filament_id1 = vertex_filament_ids[face[1]]; + auto filament_id2 = vertex_filament_ids[face[2]]; + if (filament_id0 <= 1 && filament_id1 <= 1 && filament_id2 <= 2) { + continue; + } + VertexColorCase vertex_color_case; + unsigned char iso_index; + calc_vertex_color_case(filament_id0, filament_id1, filament_id2, vertex_color_case, iso_index); + switch (vertex_color_case) { + case _3_SAME_COLOR: { + std::string result; + get_real_filament_id(filament_id0, result); + volume->mmu_segmentation_facets.set_triangle_from_string(i, result); + break; + } + case _3_DIFF_COLOR: { + std::string result0, result1, result2; + get_real_filament_id(filament_id0, result0); + get_real_filament_id(filament_id1, result1); + get_real_filament_id(filament_id2, result2); + + auto v0 = volume->mesh().its.vertices[face[0]]; + auto v1 = volume->mesh().its.vertices[face[1]]; + auto v2 = volume->mesh().its.vertices[face[2]]; + auto dir_0_1 = (v1 - v0).normalized().eval(); + auto dir_0_2 = (v2 - v0).normalized().eval(); + float sita0 = acos(dir_0_1.dot(dir_0_2)); + auto dir_1_0 = (-dir_0_1).eval(); + auto dir_1_2 = (v2 - v1).normalized().eval(); + float sita1 = acos(dir_1_0.dot(dir_1_2)); + float sita2 = PI - sita0 - sita1; + std::array sitas = {sita0, sita1, sita2}; + float max_sita = sitas[0]; + int max_sita_vertex_index = 0; + for (size_t j = 1; j < sitas.size(); j++) { + if (sitas[j] > max_sita) { + max_sita_vertex_index = j; + max_sita = sitas[j]; } } - return true; + if (max_sita_vertex_index == 0) { + volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result2 + (result1 + result2 + "5" )+ "3"); //"1C0C2C0C1C13" + } else if (max_sita_vertex_index == 1) { + volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result2 + (result0 + result2 + "9") + "3"); + } else{// if (max_sita_vertex_index == 2) + volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result2 + (result1 + result0 + "1") + "3"); + } + break; + } + case _2_SAME_1_DIFF_COLOR: { + std::string result0, result1, result2; + get_real_filament_id(filament_id0, result0); + get_real_filament_id(filament_id1, result1); + get_real_filament_id(filament_id2, result2); + if (iso_index == 0) { + volume->mmu_segmentation_facets.set_triangle_from_string(i, result0 + result1 + result1 + "2"); + } else if (iso_index == 1) { + volume->mmu_segmentation_facets.set_triangle_from_string(i, result1 + result0 + result0 + "6"); + } else if (iso_index == 2) { + volume->mmu_segmentation_facets.set_triangle_from_string(i, result2 + result0 + result0 + "A"); + } + break; + } + default: break; } } - return false; + return true; } bool Model::obj_import_face_color_deal(const std::vector &face_filament_ids, const unsigned char &first_extruder_id, Model *model) @@ -3429,6 +4000,14 @@ void FacetsAnnotation::get_facets(const ModelVolume& mv, std::vector>& facets_per_type) const +{ + TriangleSelector selector(mv.mesh()); + selector.deserialize(m_data, false); + selector.get_facet_triangles(facets_per_type); +} + void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv, EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament, diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 5974ca4cd6..cac890738a 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -29,6 +29,7 @@ #include "Format/OBJ.hpp" #include +#include #include #include #include @@ -735,6 +736,8 @@ public: indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; // BBS void get_facets(const ModelVolume& mv, std::vector& facets_per_type) const; + void get_facet_triangles(const ModelVolume& mv, + std::vector>& facets_per_type) const; void set_enforcer_block_type_limit(const ModelVolume &mv, EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE, @@ -874,6 +877,14 @@ public: // List of mesh facets painted for MMU segmentation. FacetsAnnotation mmu_segmentation_facets; + std::vector imported_vertex_colors_rgba; + + std::vector imported_texture_uvs_per_face; + std::vector imported_texture_uv_valid; + std::vector imported_texture_rgba; + uint32_t imported_texture_width{0}; + uint32_t imported_texture_height{0}; + // List of mesh facets painted for fuzzy skin. FacetsAnnotation fuzzy_skin_facets; @@ -1102,6 +1113,12 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), + imported_vertex_colors_rgba(other.imported_vertex_colors_rgba), + imported_texture_uvs_per_face(other.imported_texture_uvs_per_face), + imported_texture_uv_valid(other.imported_texture_uv_valid), + imported_texture_rgba(other.imported_texture_rgba), + imported_texture_width(other.imported_texture_width), + imported_texture_height(other.imported_texture_height), fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { assert(this->id().valid()); @@ -1186,6 +1203,8 @@ private: t = mmu_segmentation_facets.timestamp(); cereal::load_by_value(ar, mmu_segmentation_facets); mesh_changed |= t != mmu_segmentation_facets.timestamp(); + ar(imported_vertex_colors_rgba); + ar(imported_texture_uvs_per_face, imported_texture_uv_valid, imported_texture_rgba, imported_texture_width, imported_texture_height); cereal::load_by_value(ar, fuzzy_skin_facets); mesh_changed |= t != fuzzy_skin_facets.timestamp(); cereal::load_by_value(ar, config); @@ -1208,6 +1227,8 @@ private: cereal::save_by_value(ar, supported_facets); cereal::save_by_value(ar, seam_facets); cereal::save_by_value(ar, mmu_segmentation_facets); + ar(imported_vertex_colors_rgba); + ar(imported_texture_uvs_per_face, imported_texture_uv_valid, imported_texture_rgba, imported_texture_width, imported_texture_height); cereal::save_by_value(ar, fuzzy_skin_facets); cereal::save_by_value(ar, config); cereal::save(ar, text_configuration); @@ -1596,10 +1617,14 @@ public: ImportstlProgressFn stlFn = nullptr, BBLProject * project = nullptr, int plate_id = 0, - ObjImportColorFn objFn = nullptr - ); + ObjImportColorFn objFn = nullptr, + ObjImportModeFn objModeFn = nullptr + ); // BBS static bool obj_import_vertex_color_deal(const std::vector &vertex_filament_ids, const unsigned char &first_extruder_id, Model *model); + static bool obj_import_vertex_color_deal_for_object(const std::vector &vertex_filament_ids, + const unsigned char &first_extruder_id, + ModelObject *object); static bool obj_import_face_color_deal(const std::vector &face_filament_ids, const unsigned char &first_extruder_id, Model *model); static double findMaxSpeed(const ModelObject* object); static double getThermalLength(const ModelVolume* modelVolumePtr); diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 4946ad45c8..0067284a82 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -43,6 +43,19 @@ static inline Point mk_point(const Vec2d &point) { return {coord_t(std::round(po static inline Vec2d mk_vec2(const voronoi_diagram::vertex_type *point) { return {point->x(), point->y()}; } +static bool filament_id_uses_texture_mapping(const Print &print, unsigned int filament_id) +{ + if (filament_id == 0) + return false; + + const size_t num_physical = print.config().filament_diameter.size(); + if (num_physical == 0) + return false; + + const TextureMappingZone *zone = print.texture_mapping_manager().zone_from_id(filament_id); + return zone != nullptr && zone->enabled && !zone->deleted && zone->is_image_texture(); +} + static bool vertex_equal_to_point(const Voronoi::VD::vertex_type &vertex, const Vec2d &ipt) { // Convert ipt to doubles, force the 80bit FPU temporary to 64bit and then compare. @@ -1347,8 +1360,14 @@ static inline std::vector> segmentation_top_and_bottom_l // As this region may split existing regions, we collect statistics over all regions for color_idx == 0. color_idx == 0 || config.wall_filament == int(color_idx)) { //BBS: the extrusion line width is outer wall rather than inner wall - const double nozzle_diameter = print_object.print()->config().nozzle_diameter.get_at(0); + const Print &print = *print_object.print(); + const double nozzle_diameter = print.config().nozzle_diameter.get_at(0); double outer_wall_line_width = config.get_abs_value("outer_wall_line_width", nozzle_diameter); + const unsigned int queried_filament_id = color_idx == 0 ? + unsigned(std::max(0, config.wall_filament.value)) : + unsigned(color_idx); + if (filament_id_uses_texture_mapping(print, queried_filament_id)) + outer_wall_line_width = std::max(0.05, config.texture_mapping_outer_wall_gradient_max_line_width.value); out.extrusion_width = std::max(out.extrusion_width, outer_wall_line_width); out.top_shell_layers = std::max(out.top_shell_layers, config.top_shell_layers); out.bottom_shell_layers = std::max(out.bottom_shell_layers, config.bottom_shell_layers); @@ -2195,7 +2214,9 @@ std::vector> segmentation_by_painting(const PrintObject // Returns multi-material segmentation based on painting in multi-material segmentation gizmo std::vector> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function &throw_on_cancel_callback) { - const size_t num_facets_states = print_object.print()->config().filament_colour.size() + 1; + const size_t num_physical_filaments = print_object.print()->config().filament_colour.size(); + const size_t num_total_filaments = print_object.print()->texture_mapping_manager().total_filaments(num_physical_filaments); + const size_t num_facets_states = num_total_filaments + 1; const float max_width = float(print_object.config().mmu_segmented_region_max_width.value); const float interlocking_depth = float(print_object.config().mmu_segmented_region_interlocking_depth.value); const bool interlocking_beam = print_object.config().interlocking_beam.value; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index bb004ba1e2..856bd77e1c 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -41,6 +41,7 @@ static std::vector s_project_options { "filament_colour", "filament_colour_type", "filament_multi_colour", + "texture_mapping_definitions", "wipe_tower_x", "wipe_tower_y", "wipe_tower_rotation_angle", @@ -305,6 +306,8 @@ PresetBundle::PresetBundle() this->printers.select_preset(0); this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options); + if (const auto *color_opt = this->project_config.option("filament_colour", false); color_opt != nullptr) + this->texture_mapping_zones.load_entries(this->project_config.opt_string("texture_mapping_definitions"), color_opt->values); } PresetBundle::PresetBundle(const PresetBundle &rhs) @@ -323,6 +326,7 @@ PresetBundle& PresetBundle::operator=(const PresetBundle &rhs) filament_presets = rhs.filament_presets; project_config = rhs.project_config; + texture_mapping_zones = rhs.texture_mapping_zones; vendors = rhs.vendors; obsolete_presets = rhs.obsolete_presets; m_errors = rhs.m_errors; @@ -2224,6 +2228,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne } update_multi_material_filament_presets(); + texture_mapping_zones.refresh(filament_color->values); + project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_mapping_zones.serialize_entries())); } void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) { @@ -2261,6 +2267,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) } update_multi_material_filament_presets(); + texture_mapping_zones.refresh(filament_color->values); + project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_mapping_zones.serialize_entries())); } void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) @@ -2312,6 +2320,9 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) erase_or_resize(ams_multi_color_filment); update_multi_material_filament_presets(to_del_flament_id); + texture_mapping_zones.remove_physical_filament(to_del_flament_id + 1); + texture_mapping_zones.refresh(filament_color->values); + project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_mapping_zones.serialize_entries())); } @@ -3744,6 +3755,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // 4) Load the project config values (the per extruder wipe matrix etc). this->project_config.apply_only(config, s_project_options); + if (const auto *color_opt = this->project_config.option("filament_colour", false); color_opt != nullptr) + this->texture_mapping_zones.load_entries(this->project_config.opt_string("texture_mapping_definitions"), color_opt->values); break; } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 528b7cf636..d3c6738531 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -4,6 +4,7 @@ #include "Preset.hpp" #include "AppConfig.hpp" #include "enum_bitmask.hpp" +#include "TextureMapping.hpp" #include #include @@ -223,6 +224,7 @@ public: // they are being serialized / deserialized from / to the .amf, .3mf, .config, .gcode, // and they are being used by slicing core. DynamicPrintConfig project_config; + TextureMappingManager texture_mapping_zones; // There will be an entry for each system profile loaded, // and the system profiles will point to the VendorProfile instances owned by PresetBundle::vendors. diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ad6e279cd4..4d409b26d0 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -142,6 +142,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "filament_density", "filament_cost", "filament_notes", + "texture_mapping_outer_wall_gradient_global_strength", + "texture_mapping_outer_wall_gradient_max_line_width", + "texture_mapping_outer_wall_gradient_min_line_width", + "texture_mapping_definitions", "outer_wall_acceleration", "inner_wall_acceleration", "initial_layer_acceleration", @@ -455,9 +459,16 @@ std::vector Print::object_extruders() const for (const PrintObject* object : m_objects) { const ModelObject* mo = object->model_object(); + const size_t num_physical = m_config.filament_colour.size(); + auto resolve_filament_id = [this, num_physical](int filament_id) { + if (filament_id > 0 && m_texture_mapping_mgr.is_texture_mapping_zone_id(unsigned(filament_id))) + return int(m_texture_mapping_mgr.resolve_zone_component(unsigned(filament_id), num_physical, 0)); + return filament_id; + }; for (const ModelVolume* mv : mo->volumes) { std::vector volume_extruders = mv->get_extruders(); for (int extruder : volume_extruders) { + extruder = resolve_filament_id(extruder); assert(extruder > 0); extruders.push_back(extruder - 1); } @@ -470,6 +481,7 @@ std::vector Print::object_extruders() const //Don't know why height range always save key "extruder" because of no change(should only save difference)... //Add protection here to avoid overflow auto value = layer_range.second.option("extruder")->getInt(); + value = resolve_filament_id(value); if (value > 0) extruders.push_back(value - 1); } @@ -522,11 +534,16 @@ std::vector Print::extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS - int num_extruders = m_config.filament_colour.size(); + const size_t num_physical = m_config.filament_colour.size(); if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) - extruders.push_back((unsigned int)(item.extruder - 1)); + if (item.type != CustomGCode::Type::ToolChange || item.extruder <= 0) + continue; + int extruder_id = item.extruder; + if (m_texture_mapping_mgr.is_texture_mapping_zone_id(unsigned(extruder_id))) + extruder_id = int(m_texture_mapping_mgr.resolve_zone_component(unsigned(extruder_id), num_physical, 0)); + if (extruder_id > 0 && extruder_id <= int(num_physical)) + extruders.push_back((unsigned int)(extruder_id - 1)); } } } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b98811c8e3..50e945d083 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -17,6 +17,7 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "TextureMapping.hpp" #include "libslic3r.h" #include @@ -943,6 +944,8 @@ public: void auto_assign_extruders(ModelObject* model_object) const; const PrintConfig& config() const { return m_config; } + const TextureMappingManager& texture_mapping_manager() const { return m_texture_mapping_mgr; } + TextureMappingManager& texture_mapping_manager() { return m_texture_mapping_mgr; } const PrintObjectConfig& default_object_config() const { return m_default_object_config; } const PrintRegionConfig& default_region_config() const { return m_default_region_config; } ConstPrintObjectPtrsAdaptor objects() const { return ConstPrintObjectPtrsAdaptor(&m_objects); } @@ -1127,6 +1130,7 @@ private: Polygons first_layer_islands() const; PrintConfig m_config; + TextureMappingManager m_texture_mapping_mgr; PrintObjectConfig m_default_object_config; PrintRegionConfig m_default_region_config; PrintObjectPtrs m_objects; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index ac2d56780f..7466964cc4 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1116,6 +1116,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ new_full_config.option("print_settings_id", true); new_full_config.option("filament_settings_id", true); new_full_config.option("printer_settings_id", true); + new_full_config.option("texture_mapping_definitions", true); // BBS std::vector used_filaments = this->extruders(true); @@ -1227,7 +1228,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } // Grab the lock for the Print / PrintObject milestones. - std::scoped_lock lock(this->state_mutex()); + std::scoped_lock lock(this->state_mutex()); + if (const ConfigOptionStrings *color_opt = new_full_config.option("filament_colour", false); color_opt != nullptr) + m_texture_mapping_mgr.load_entries(new_full_config.opt_string("texture_mapping_definitions"), color_opt->values); // The following call may stop the background processing. if (! print_diff.empty()) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 508e7c805f..a917402db5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2331,6 +2331,13 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings{ "#F2754E" }); + def = this->add("texture_mapping_definitions", coString); + def->label = L("Texture mapping definitions"); + def->tooltip = L("Serialized texture mapping rows."); + def->gui_flags = "serialized"; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("")); + // PS def = this->add("filament_notes", coStrings); def->label = L("Filament notes"); @@ -4635,6 +4642,36 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(1)); + def = this->add("texture_mapping_outer_wall_gradient_global_strength", coFloat); + def->label = L("Outer wall offset gradient global strength"); + def->category = L("Multimaterial"); + def->tooltip = L("Global strength multiplier for texture mapping outer wall offset gradients."); + def->sidetext = "%"; + def->min = 0.0; + def->max = 100.0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(100.0)); + + def = this->add("texture_mapping_outer_wall_gradient_max_line_width", coFloat); + def->label = L("Maximum outer wall line width"); + def->category = L("Multimaterial"); + def->tooltip = L("Upper bound for external perimeter line width used by texture mapping."); + def->sidetext = "mm"; + def->min = 0.05; + def->max = 3.0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.95)); + + def = this->add("texture_mapping_outer_wall_gradient_min_line_width", coFloat); + def->label = L("Minimum outer wall line width"); + def->category = L("Multimaterial"); + def->tooltip = L("Lower bound for external perimeter line width used by texture mapping outer wall gradient effects."); + def->sidetext = "mm"; + def->min = 0.05; + def->max = 2.0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.32)); + def = this->add("inner_wall_line_width", coFloatOrPercent); def->label = L("Inner wall"); def->category = L("Quality"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index af483f1c9e..afebb7cd43 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1118,6 +1118,9 @@ PRINT_CONFIG_CLASS_DEFINE( // Detect bridging perimeters ((ConfigOptionBool, detect_overhang_wall)) ((ConfigOptionInt, wall_filament)) + ((ConfigOptionFloat, texture_mapping_outer_wall_gradient_global_strength)) + ((ConfigOptionFloat, texture_mapping_outer_wall_gradient_max_line_width)) + ((ConfigOptionFloat, texture_mapping_outer_wall_gradient_min_line_width)) ((ConfigOptionFloatOrPercent, inner_wall_line_width)) ((ConfigOptionFloat, inner_wall_speed)) // Total number of perimeters. @@ -1290,6 +1293,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionIntsNullable, nozzle_flush_dataset)) ((ConfigOptionFloatsNullable, filament_flush_volumetric_speed)) ((ConfigOptionIntsNullable, filament_flush_temp)) + ((ConfigOptionString, texture_mapping_definitions)) // BBS ((ConfigOptionBool, scan_first_layer)) ((ConfigOptionEnum, enable_power_loss_recovery)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index a08a7d74b0..bc2980ce14 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3304,9 +3304,14 @@ void PrintObject::bridge_over_infill() } // void PrintObject::bridge_over_infill() -static void clamp_exturder_to_default(ConfigOptionInt &opt, size_t num_extruders) +static bool is_texture_mapping_virtual_filament_id(int filament_id) { - if (opt.value > (int)num_extruders) + return filament_id >= 99 && filament_id <= 255; +} + +static void clamp_exturder_to_default(ConfigOptionInt &opt, size_t num_extruders, bool allow_texture_mapping_virtual_id = false) +{ + if (opt.value > (int)num_extruders && !(allow_texture_mapping_virtual_id && is_texture_mapping_virtual_filament_id(opt.value))) // assign the default extruder opt.value = 1; } @@ -3372,9 +3377,9 @@ PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &defau apply_to_print_region_config(config, *layer_range_config); } // Clamp invalid extruders to the default extruder (with index 1). - clamp_exturder_to_default(config.sparse_infill_filament, num_extruders); - clamp_exturder_to_default(config.wall_filament, num_extruders); - clamp_exturder_to_default(config.solid_infill_filament, num_extruders); + clamp_exturder_to_default(config.sparse_infill_filament, num_extruders, true); + clamp_exturder_to_default(config.wall_filament, num_extruders, true); + clamp_exturder_to_default(config.solid_infill_filament, num_extruders, true); if (config.sparse_infill_density.value < 0.00011f) // Switch of infill for very low infill rates, also avoid division by zero in infill generator for these very low rates. // See GH issue #5910. diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2d67ac0939..12236b3079 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,5 +1,10 @@ #include +#include +#include +#include +#include + #include #include "ClipperUtils.hpp" @@ -54,7 +59,8 @@ static std::vector slice_volume( { std::vector layers; if (! zs.empty()) { - indexed_triangle_set its = volume.mesh().its; + const std::shared_ptr mesh_ptr = volume.mesh_ptr(); + indexed_triangle_set its = mesh_ptr ? mesh_ptr->its : indexed_triangle_set(); if (its.indices.size() > 0) { MeshSlicingParamsEx params2 { params }; params2.trafo = params2.trafo * volume.get_matrix(); @@ -113,6 +119,224 @@ static inline bool model_volume_needs_slicing(const ModelVolume &mv) return type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER; } +static std::vector collect_texture_mapping_outer_wall_gradient_line_width_warnings(const PrintObject &print_object) +{ + const Print *print = print_object.print(); + if (print == nullptr) + return {}; + + bool has_offset_profiles = false; + for (const TextureMappingZone &zone : print->texture_mapping_manager().zones()) { + if (!zone.enabled || zone.deleted) + continue; + if (zone.is_2d_gradient() || zone.is_image_texture() || zone.has_custom_offset_settings()) { + has_offset_profiles = true; + break; + } + } + if (!has_offset_profiles) + return {}; + + if (print_object.num_printing_regions() == 0) + return {}; + + float max_gradient_line_width_mm = 0.f; + float min_gradient_line_width_mm = std::numeric_limits::max(); + for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++region_id) { + const PrintRegionConfig ®ion_cfg = print_object.printing_region(region_id).config(); + max_gradient_line_width_mm = std::max(max_gradient_line_width_mm, + float(region_cfg.texture_mapping_outer_wall_gradient_max_line_width.value)); + min_gradient_line_width_mm = std::min(min_gradient_line_width_mm, + float(region_cfg.texture_mapping_outer_wall_gradient_min_line_width.value)); + } + max_gradient_line_width_mm = std::max(0.f, max_gradient_line_width_mm); + min_gradient_line_width_mm = std::max(0.f, min_gradient_line_width_mm); + std::vector warnings; + warnings.reserve(2); + + bool warned_min_line_width = false; + bool warned_gradient_width_range = false; + const float gradient_line_width_range_mm = std::max(0.f, max_gradient_line_width_mm - min_gradient_line_width_mm); + for (double nozzle_diameter_mm : print->config().nozzle_diameter.values) { + const float nozzle_mm = std::max(0.01f, float(nozzle_diameter_mm)); + + if (!warned_min_line_width && min_gradient_line_width_mm + EPSILON < 0.5f * nozzle_mm) { + warnings.emplace_back( + L("Minimum outer wall line width is below 50% of nozzle diameter. " + "Increase it to improve extrusion stability.")); + warned_min_line_width = true; + } + + if (!warned_gradient_width_range && gradient_line_width_range_mm + EPSILON < 0.2f) { + warnings.emplace_back( + L("Texture mapping outer wall line width range is below 0.2mm. Increase the difference between minimum and " + "maximum outer wall line width in multimaterial options for stronger gradient effects.")); + warned_gradient_width_range = true; + } + + if (warned_min_line_width && warned_gradient_width_range) + break; + } + + return warnings; +} + +static std::vector collect_texture_mapping_vertex_color_match_warnings(const PrintObject &print_object) +{ + const Print *print = print_object.print(); + if (print == nullptr) + return {}; + + const ModelObject *model_object = print_object.model_object(); + if (model_object == nullptr) + return {}; + + bool object_uses_vertex_match_mode = false; + for (const ModelVolume *volume : model_object->volumes) { + if (volume == nullptr) + continue; + + const std::vector used_extruders = volume->get_extruders(); + for (const int filament_id : used_extruders) { + if (filament_id <= 0) + continue; + const unsigned int filament_id_u = unsigned(filament_id); + const TextureMappingZone *zone = print->texture_mapping_manager().zone_from_id(filament_id_u); + if (zone != nullptr && zone->enabled && !zone->deleted && zone->is_image_texture()) { + object_uses_vertex_match_mode = true; + break; + } + } + if (object_uses_vertex_match_mode) + break; + } + + if (!object_uses_vertex_match_mode) + return {}; + + bool has_imported_vertex_color_data = false; + bool has_imported_texture_data = false; + bool has_uv_texture_reference_but_no_image = false; + for (const ModelVolume *volume : model_object->volumes) { + if (volume != nullptr && !volume->imported_vertex_colors_rgba.empty()) + has_imported_vertex_color_data = true; + if (volume != nullptr && + !volume->imported_texture_uv_valid.empty() && + !volume->imported_texture_uvs_per_face.empty() && + (volume->imported_texture_rgba.empty() || + volume->imported_texture_width == 0 || + volume->imported_texture_height == 0)) { + has_uv_texture_reference_but_no_image = true; + } + if (volume != nullptr && + !volume->imported_texture_rgba.empty() && + volume->imported_texture_width > 0 && + volume->imported_texture_height > 0 && + !volume->imported_texture_uv_valid.empty() && + !volume->imported_texture_uvs_per_face.empty()) { + has_imported_texture_data = true; + } + if (has_imported_vertex_color_data || has_imported_texture_data) + break; + } + + if (has_imported_vertex_color_data || has_imported_texture_data) + return {}; + + if (has_uv_texture_reference_but_no_image) + return { + L("Image Texture Mapping is used on this object and OBJ UVs were found, but the texture image could not be loaded. " + "Texture color matching will be skipped for this object. " + "(This importer path currently expects a PNG image texture.)") + }; + + return { + L("Image Texture Mapping is used on this object, but no imported vertex colors or OBJ UV texture data were found. " + "Texture color matching will be skipped for this object. " + "(This importer path currently expects a PNG image texture.)") + }; +} + +static const char *vertex_color_mode_name_for_error(int filament_color_mode) +{ + switch (filament_color_mode) { + case int(TextureMappingZone::FilamentColorRGB): + return "RGB"; + case int(TextureMappingZone::FilamentColorCMY): + return "CMY"; + case int(TextureMappingZone::FilamentColorCMYK): + return "CMYK"; + case int(TextureMappingZone::FilamentColorCMYW): + return "CMYW"; + case int(TextureMappingZone::FilamentColorRGBK): + return "RGBK"; + case int(TextureMappingZone::FilamentColorRGBW): + return "RGBW"; + case int(TextureMappingZone::FilamentColorBW): + return "BW"; + default: + return "Generic Solver (slow)"; + } +} + +static std::vector collect_texture_mapping_vertex_color_mode_mismatch_errors(const PrintObject &print_object) +{ + const Print *print = print_object.print(); + if (print == nullptr) + return {}; + + const ModelObject *model_object = print_object.model_object(); + if (model_object == nullptr) + return {}; + + const size_t num_physical = print->config().filament_colour.size(); + + std::vector errors; + std::set seen_zone_ids; + for (const ModelVolume *volume : model_object->volumes) { + if (volume == nullptr) + continue; + + const std::vector used_extruders = volume->get_extruders(); + for (const int filament_id : used_extruders) { + if (filament_id <= 0) + continue; + + const unsigned int filament_id_u = unsigned(filament_id); + if (seen_zone_ids.find(filament_id_u) != seen_zone_ids.end()) + continue; + seen_zone_ids.insert(filament_id_u); + + const TextureMappingZone *zone = print->texture_mapping_manager().zone_from_id(filament_id_u); + if (zone == nullptr || !zone->enabled || zone->deleted || !zone->is_image_texture()) + continue; + + const int filament_color_mode = std::clamp(zone->filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + const size_t expected_count = TextureMappingManager::expected_component_count(zone->texture_mapping_mode, + filament_color_mode); + if (expected_count == 0) + continue; + + const std::vector selected_ids = TextureMappingManager::selected_component_ids(*zone, num_physical); + if (selected_ids.size() == expected_count) + continue; + + errors.emplace_back( + L("Image Texture Mapping is used with an incompatible 'Filament colors' mode. ") + + L("Texture mapping zone ID ") + std::to_string(filament_id_u) + + L(" uses mode '") + vertex_color_mode_name_for_error(filament_color_mode) + + L("' which requires ") + std::to_string(expected_count) + + L(" selected horizontal filaments, but ") + std::to_string(selected_ids.size()) + + L(" are selected.") + ); + } + } + + return errors; +} + // Slice printable volumes, negative volumes and modifier volumes, sorted by ModelVolume::id(). // Apply closing radius. // Apply positive XY compensation to ModelVolumeType::MODEL_PART and ModelVolumeType::PARAMETER_MODIFIER, not to ModelVolumeType::NEGATIVE_VOLUME. @@ -1176,6 +1400,12 @@ void PrintObject::slice_volumes() m_print->throw_if_canceled(); this->apply_conical_overhang(); + for (const std::string &warning_msg : collect_texture_mapping_outer_wall_gradient_line_width_warnings(*this)) + this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); + for (const std::string &warning_msg : collect_texture_mapping_vertex_color_match_warnings(*this)) + this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); + for (const std::string &error_msg : collect_texture_mapping_vertex_color_mode_mismatch_errors(*this)) + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, error_msg); // Is any ModelVolume multi-material painted? if (const auto& volumes = this->model_object()->volumes; diff --git a/src/libslic3r/PrintRegion.cpp b/src/libslic3r/PrintRegion.cpp index f3d5359f4c..0bab40bb8f 100644 --- a/src/libslic3r/PrintRegion.cpp +++ b/src/libslic3r/PrintRegion.cpp @@ -1,8 +1,22 @@ #include "Exception.hpp" #include "Print.hpp" +#include "TextureMapping.hpp" namespace Slic3r { +static bool filament_id_uses_texture_mapping(const Print &print, unsigned int filament_id) +{ + if (filament_id == 0) + return false; + + const size_t num_physical = print.config().filament_diameter.size(); + if (num_physical == 0) + return false; + + const TextureMappingZone *zone = print.texture_mapping_manager().zone_from_id(filament_id); + return zone != nullptr && zone->enabled && !zone->deleted && zone->is_image_texture(); +} + // 1-based extruder identifier for this region and role. unsigned int PrintRegion::extruder(FlowRole role) const { @@ -24,7 +38,12 @@ Flow PrintRegion::flow(const PrintObject &object, FlowRole role, double layer_he ConfigOptionFloatOrPercent config_width; // Get extrusion width from configuration. // (might be an absolute value, or a percent value, or zero for auto) - if (first_layer && print_config.initial_layer_line_width.value > 0) { + if (role == frExternalPerimeter && + filament_id_uses_texture_mapping(*object.print(), unsigned(std::max(0, m_config.wall_filament.value)))) { + config_width = ConfigOptionFloatOrPercent( + std::max(0.05, m_config.texture_mapping_outer_wall_gradient_max_line_width.value), + false); + } else if (first_layer && print_config.initial_layer_line_width.value > 0) { config_width = print_config.initial_layer_line_width; } else if (role == frExternalPerimeter) { config_width = m_config.outer_wall_line_width; @@ -85,11 +104,21 @@ void PrintRegion::collect_object_printing_extruders(const Print &print, std::vec #ifndef NDEBUG // BBS auto num_extruders = int(print.config().filament_diameter.size()); - assert(this->config().wall_filament <= num_extruders); - assert(this->config().sparse_infill_filament <= num_extruders); - assert(this->config().solid_infill_filament <= num_extruders); + assert(this->config().wall_filament <= num_extruders || print.texture_mapping_manager().is_texture_mapping_zone_id(this->config().wall_filament)); + assert(this->config().sparse_infill_filament <= num_extruders || print.texture_mapping_manager().is_texture_mapping_zone_id(this->config().sparse_infill_filament)); + assert(this->config().solid_infill_filament <= num_extruders || print.texture_mapping_manager().is_texture_mapping_zone_id(this->config().solid_infill_filament)); #endif - collect_object_printing_extruders(print.config(), this->config(), print.has_brim(), object_extruders); + PrintRegionConfig config = this->config(); + const size_t num_physical = print.config().filament_colour.size(); + auto resolve_filament_id = [&print, num_physical](int filament_id) { + if (filament_id > 0 && print.texture_mapping_manager().is_texture_mapping_zone_id(unsigned(filament_id))) + return int(print.texture_mapping_manager().resolve_zone_component(unsigned(filament_id), num_physical, 0)); + return filament_id; + }; + config.wall_filament.value = resolve_filament_id(config.wall_filament.value); + config.sparse_infill_filament.value = resolve_filament_id(config.sparse_infill_filament.value); + config.solid_infill_filament.value = resolve_filament_id(config.solid_infill_filament.value); + collect_object_printing_extruders(print.config(), config, print.has_brim(), object_extruders); } } diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp new file mode 100644 index 0000000000..c795f30017 --- /dev/null +++ b/src/libslic3r/TextureMapping.cpp @@ -0,0 +1,1310 @@ +#include "TextureMapping.hpp" +#include "filament_mixer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +constexpr unsigned int TextureMappingZoneIdBase = 99; +constexpr unsigned int MaxTextureMappingZoneId = 255; +constexpr const char *TextureMappingGapDisplayColor = "#8C8C8C"; + +struct RGB { + int r = 0; + int g = 0; + int b = 0; +}; + +static int clamp_int(int value, int lo, int hi) +{ + return std::max(lo, std::min(hi, value)); +} + +static float finite_or(float value, float fallback) +{ + return std::isfinite(value) ? value : fallback; +} + +static RGB parse_hex_color(const std::string &hex) +{ + RGB c; + if (hex.size() >= 7 && hex[0] == '#') { + try { + c.r = std::stoi(hex.substr(1, 2), nullptr, 16); + c.g = std::stoi(hex.substr(3, 2), nullptr, 16); + c.b = std::stoi(hex.substr(5, 2), nullptr, 16); + } catch (...) { + c = {}; + } + } + return c; +} + +static std::string rgb_to_hex(const RGB &c) +{ + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", + std::clamp(c.r, 0, 255), + std::clamp(c.g, 0, 255), + std::clamp(c.b, 0, 255)); + return std::string(buf); +} + +static std::string random_display_color(uint64_t stable_id) +{ + uint64_t x = stable_id == 0 ? 0x6A09E667F3BCC909ull : stable_id; + x ^= x >> 33; + x *= 0xff51afd7ed558ccdull; + x ^= x >> 33; + x *= 0xc4ceb9fe1a85ec53ull; + x ^= x >> 33; + + const int r = 80 + int((x >> 0) & 0x7f); + const int g = 80 + int((x >> 8) & 0x7f); + const int b = 80 + int((x >> 16) & 0x7f); + return rgb_to_hex({r, g, b}); +} + +static std::vector decode_component_ids(const std::string &encoded, size_t num_physical) +{ + std::vector ids; + bool seen[10] = { false }; + const unsigned int max_id = unsigned(std::min(num_physical, 9)); + for (char c : encoded) { + if (c < '1' || c > '9') + continue; + const unsigned int id = unsigned(c - '0'); + if (id > max_id || seen[id]) + continue; + seen[id] = true; + ids.emplace_back(id); + } + return ids; +} + +static std::string encode_component_ids(const std::vector &ids) +{ + std::string encoded; + bool seen[10] = { false }; + for (const unsigned int id : ids) { + if (id == 0 || id > 9 || seen[id]) + continue; + seen[id] = true; + encoded.push_back(char('0' + id)); + } + return encoded; +} + +static std::vector parse_int_tokens(const std::string &value) +{ + std::vector out; + std::string current; + for (const char c : value) { + if (std::isdigit(static_cast(c)) || (c == '-' && current.empty())) { + current.push_back(c); + continue; + } + if (!current.empty() && current != "-") { + try { + out.emplace_back(std::stoi(current)); + } catch (...) { + } + } + current.clear(); + } + if (!current.empty() && current != "-") { + try { + out.emplace_back(std::stoi(current)); + } catch (...) { + } + } + return out; +} + +static std::vector parse_float_tokens(const std::string &value) +{ + std::vector out; + std::string current; + bool has_digit = false; + for (const char c : value) { + const bool token_char = std::isdigit(static_cast(c)) || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E'; + if (token_char) { + current.push_back(c); + if (std::isdigit(static_cast(c))) + has_digit = true; + continue; + } + if (has_digit) { + try { + out.emplace_back(std::stof(current)); + } catch (...) { + } + } + current.clear(); + has_digit = false; + } + if (has_digit) { + try { + out.emplace_back(std::stof(current)); + } catch (...) { + } + } + return out; +} + +static std::string format_float_token(float value) +{ + std::ostringstream ss; + ss << std::fixed << std::setprecision(4) << value; + std::string out = ss.str(); + while (!out.empty() && out.back() == '0') + out.pop_back(); + if (!out.empty() && out.back() == '.') + out.pop_back(); + return out.empty() ? "0" : out; +} + +static std::string normalize_weights(const std::string &weights, size_t expected_count) +{ + if (expected_count == 0) + return std::string(); + + std::vector parsed = parse_int_tokens(weights); + if (parsed.size() != expected_count) + return std::string(); + + int total = 0; + for (int &weight : parsed) { + weight = std::max(0, weight); + total += weight; + } + if (total <= 0) + return std::string(); + + std::ostringstream ss; + for (size_t i = 0; i < parsed.size(); ++i) { + if (i > 0) + ss << '/'; + ss << parsed[i]; + } + return ss.str(); +} + +static std::vector decode_weights(const std::string &weights, size_t expected_count) +{ + std::vector parsed = parse_int_tokens(weights); + if (parsed.size() != expected_count) + return {}; + int total = 0; + for (int &weight : parsed) { + weight = std::max(0, weight); + total += weight; + } + return total > 0 ? parsed : std::vector(); +} + +static int safe_mod(int value, int divisor) +{ + if (divisor <= 0) + return 0; + int out = value % divisor; + if (out < 0) + out += divisor; + return out; +} + +static std::vector build_balanced_component_sequence(const std::vector &ids, + const std::vector &weights) +{ + if (ids.empty()) + return {}; + + std::vector counts; + counts.reserve(ids.size()); + for (size_t i = 0; i < ids.size(); ++i) { + const int weight = i < weights.size() ? std::max(0, weights[i]) : 1; + counts.emplace_back(weight); + } + if (std::all_of(counts.begin(), counts.end(), [](int v) { return v <= 0; })) + counts.assign(ids.size(), 1); + + int total = std::accumulate(counts.begin(), counts.end(), 0); + constexpr int MaxCycle = 64; + if (total > MaxCycle) { + const double scale = double(MaxCycle) / double(total); + for (int &count : counts) + count = count <= 0 ? 0 : std::max(1, int(std::lround(double(count) * scale))); + total = std::accumulate(counts.begin(), counts.end(), 0); + } + if (total <= 0) + return {}; + + std::vector sequence; + sequence.reserve(size_t(total)); + std::vector debt(ids.size(), 0); + for (int step = 0; step < total; ++step) { + size_t best_idx = 0; + int best_debt = std::numeric_limits::lowest(); + for (size_t idx = 0; idx < counts.size(); ++idx) { + debt[idx] += counts[idx]; + if (debt[idx] > best_debt) { + best_debt = debt[idx]; + best_idx = idx; + } + } + sequence.emplace_back(ids[best_idx]); + debt[best_idx] -= total; + } + return sequence; +} + +static std::string normalize_offset_distances(const std::string &distances, size_t expected_count, float max_distance_mm) +{ + if (expected_count == 0) + return std::string(); + std::vector parsed = parse_float_tokens(distances); + if (parsed.size() != expected_count) + return std::string(); + + std::ostringstream ss; + for (size_t i = 0; i < parsed.size(); ++i) { + if (i > 0) + ss << '/'; + ss << format_float_token(std::clamp(finite_or(parsed[i], 0.f), 0.f, max_distance_mm)); + } + return ss.str(); +} + +static std::string normalize_offset_angles(const std::string &angles, size_t expected_count) +{ + if (expected_count == 0) + return std::string(); + std::vector parsed = parse_float_tokens(angles); + if (parsed.size() != expected_count) + return std::string(); + + std::ostringstream ss; + for (size_t i = 0; i < parsed.size(); ++i) { + float angle = std::fmod(finite_or(parsed[i], 0.f), 360.f); + if (angle < 0.f) + angle += 360.f; + if (i > 0) + ss << '/'; + ss << format_float_token(angle); + } + return ss.str(); +} + +static std::vector decode_offset_distances(const std::string &distances, size_t expected_count, float max_distance_mm) +{ + std::vector parsed = parse_float_tokens(distances); + if (parsed.size() != expected_count) + return {}; + for (float &value : parsed) + value = std::clamp(finite_or(value, 0.f), 0.f, max_distance_mm); + return parsed; +} + +static std::vector decode_offset_angles(const std::string &angles, size_t expected_count) +{ + std::vector parsed = parse_float_tokens(angles); + if (parsed.size() != expected_count) + return {}; + for (float &value : parsed) { + value = std::fmod(finite_or(value, 0.f), 360.f); + if (value < 0.f) + value += 360.f; + } + return parsed; +} + +static std::vector normalize_strengths(const std::vector &values) +{ + std::vector out; + out.reserve(std::min(values.size(), 9)); + for (size_t i = 0; i < std::min(values.size(), 9); ++i) + out.emplace_back(std::clamp(finite_or(values[i], 100.f), 0.f, 100.f)); + while (!out.empty() && std::abs(out.back() - 100.f) <= 1e-6f) + out.pop_back(); + return out; +} + +static std::vector normalize_minimum_offsets(const std::vector &values) +{ + std::vector out; + out.reserve(std::min(values.size(), 9)); + for (size_t i = 0; i < std::min(values.size(), 9); ++i) + out.emplace_back(std::clamp(finite_or(values[i], 0.f), 0.f, 100.f)); + while (!out.empty() && std::abs(out.back()) <= 1e-6f) + out.pop_back(); + return out; +} + +static float normalize_tone_gamma(float value) +{ + return (!std::isfinite(value) || value <= 0.f) ? 1.f : std::clamp(value, 0.5f, 3.f); +} + +static float normalize_sagging_ratio(float value) +{ + return std::isfinite(value) ? std::clamp(value, 0.f, 6.f) : 0.f; +} + +static RGB filament_color(unsigned int id, const std::vector &filament_colours) +{ + if (id >= 1 && size_t(id - 1) < filament_colours.size()) + return parse_hex_color(filament_colours[size_t(id - 1)]); + return {}; +} + +static float color_distance_sq(const RGB &lhs, const RGB &rhs) +{ + const float dr = float(lhs.r - rhs.r); + const float dg = float(lhs.g - rhs.g); + const float db = float(lhs.b - rhs.b); + return dr * dr + dg * dg + db * db; +} + +static std::vector semantic_colors(int filament_color_mode) +{ + switch (clamp_int(filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW))) { + case int(TextureMappingZone::FilamentColorRGB): return {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}}; + case int(TextureMappingZone::FilamentColorCMY): return {{0, 255, 255}, {255, 0, 255}, {255, 255, 0}}; + case int(TextureMappingZone::FilamentColorCMYK): return {{0, 255, 255}, {255, 0, 255}, {255, 255, 0}, {0, 0, 0}}; + case int(TextureMappingZone::FilamentColorCMYW): return {{0, 255, 255}, {255, 0, 255}, {255, 255, 0}, {255, 255, 255}}; + case int(TextureMappingZone::FilamentColorRGBK): return {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {0, 0, 0}}; + case int(TextureMappingZone::FilamentColorRGBW): return {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}}; + case int(TextureMappingZone::FilamentColorBW): return {{0, 0, 0}, {255, 255, 255}}; + default: return {}; + } +} + +static std::vector ids_from_json(const nlohmann::json &value, size_t num_physical) +{ + std::vector ids; + if (!value.is_array()) + return ids; + bool seen[10] = { false }; + const unsigned int max_id = unsigned(std::min(num_physical, 9)); + for (const nlohmann::json &item : value) { + if (!item.is_number_integer() && !item.is_number_unsigned()) + continue; + const int raw = item.get(); + if (raw < 1 || raw > int(max_id) || seen[raw]) + continue; + seen[raw] = true; + ids.emplace_back(unsigned(raw)); + } + return ids; +} + +static nlohmann::json ids_to_json(const std::vector &ids) +{ + nlohmann::json out = nlohmann::json::array(); + for (const unsigned int id : ids) + out.push_back(id); + return out; +} + +static std::vector floats_from_json(const nlohmann::json &value) +{ + std::vector out; + if (!value.is_array()) + return out; + out.reserve(value.size()); + for (const nlohmann::json &item : value) + if (item.is_number()) + out.emplace_back(item.get()); + return out; +} + +static nlohmann::json floats_to_json(const std::vector &values) +{ + nlohmann::json out = nlohmann::json::array(); + for (const float value : values) + out.push_back(value); + return out; +} + +static nlohmann::json weights_to_json(const std::string &weights, size_t expected_count) +{ + nlohmann::json out = nlohmann::json::array(); + const std::vector parsed = decode_weights(weights, expected_count); + for (const int weight : parsed) + out.push_back(weight); + return out; +} + +static std::string weights_from_json(const nlohmann::json &value, size_t expected_count) +{ + if (!value.is_array() || expected_count == 0) + return std::string(); + std::vector values; + values.reserve(value.size()); + for (const nlohmann::json &item : value) + if (item.is_number_integer() || item.is_number_unsigned()) + values.emplace_back(std::max(0, item.get())); + + std::ostringstream ss; + for (size_t i = 0; i < values.size(); ++i) { + if (i > 0) + ss << '/'; + ss << values[i]; + } + return normalize_weights(ss.str(), expected_count); +} + +static std::string mapping_mode_name(int mode) +{ + return mode == int(TextureMappingZone::TextureMappingRawValues) ? + std::string("raw_channel_offsets") : + std::string("target_color"); +} + +static int mapping_mode_from_name(const std::string &name) +{ + return name == "raw_channel_offsets" ? + int(TextureMappingZone::TextureMappingRawValues) : + int(TextureMappingZone::TextureMappingFilamentBlending); +} + +static std::string color_model_name(int mode) +{ + switch (clamp_int(mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorBW))) { + case int(TextureMappingZone::FilamentColorRGB): return "rgb"; + case int(TextureMappingZone::FilamentColorCMY): return "cmy"; + case int(TextureMappingZone::FilamentColorCMYK): return "cmyk"; + case int(TextureMappingZone::FilamentColorCMYW): return "cmyw"; + case int(TextureMappingZone::FilamentColorRGBK): return "rgbk"; + case int(TextureMappingZone::FilamentColorRGBW): return "rgbw"; + case int(TextureMappingZone::FilamentColorBW): return "bw"; + default: return "any"; + } +} + +static int color_model_from_name(const std::string &name) +{ + if (name == "rgb") return int(TextureMappingZone::FilamentColorRGB); + if (name == "cmy") return int(TextureMappingZone::FilamentColorCMY); + if (name == "cmyk") return int(TextureMappingZone::FilamentColorCMYK); + if (name == "cmyw") return int(TextureMappingZone::FilamentColorCMYW); + if (name == "rgbk") return int(TextureMappingZone::FilamentColorRGBK); + if (name == "rgbw") return int(TextureMappingZone::FilamentColorRGBW); + if (name == "bw") return int(TextureMappingZone::FilamentColorBW); + return int(TextureMappingZone::FilamentColorAny); +} + +} // namespace + +bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const +{ + constexpr float eps = 1e-6f; + auto floats_equal = [](const std::vector &lhs, const std::vector &rhs_values) { + if (lhs.size() != rhs_values.size()) + return false; + for (size_t i = 0; i < lhs.size(); ++i) + if (std::abs(lhs[i] - rhs_values[i]) > 1e-6f) + return false; + return true; + }; + + return stable_id == rhs.stable_id && + zone_id == rhs.zone_id && + enabled == rhs.enabled && + deleted == rhs.deleted && + surface_pattern == rhs.surface_pattern && + component_a == rhs.component_a && + component_b == rhs.component_b && + component_ids == rhs.component_ids && + component_weights == rhs.component_weights && + display_color == rhs.display_color && + offset_distances == rhs.offset_distances && + offset_angles == rhs.offset_angles && + offset_mode == rhs.offset_mode && + offset_rotation_enabled == rhs.offset_rotation_enabled && + std::abs(offset_rotations - rhs.offset_rotations) <= eps && + std::abs(offset_repeats - rhs.offset_repeats) <= eps && + offset_reverse_repeats == rhs.offset_reverse_repeats && + offset_clockwise == rhs.offset_clockwise && + offset_fade_mode == rhs.offset_fade_mode && + offset_angle_mode == rhs.offset_angle_mode && + texture_mapping_mode == rhs.texture_mapping_mode && + filament_color_mode == rhs.filament_color_mode && + force_sequential_filaments == rhs.force_sequential_filaments && + reduce_outer_surface_texture == rhs.reduce_outer_surface_texture && + seam_hiding == rhs.seam_hiding && + nonlinear_offset_adjustment == rhs.nonlinear_offset_adjustment && + compact_offset_mode == rhs.compact_offset_mode && + std::abs(contrast_pct - rhs.contrast_pct) <= eps && + high_resolution_sampling == rhs.high_resolution_sampling && + std::abs(tone_gamma - rhs.tone_gamma) <= eps && + std::abs(sagging_ratio - rhs.sagging_ratio) <= eps && + std::abs(preview_opacity_pct - rhs.preview_opacity_pct) <= eps && + preview_simulate_colors == rhs.preview_simulate_colors && + preview_limit_resolution == rhs.preview_limit_resolution && + auto_adjust_filament_selection == rhs.auto_adjust_filament_selection && + floats_equal(filament_strengths_pct, rhs.filament_strengths_pct) && + floats_equal(filament_minimum_offsets_pct, rhs.filament_minimum_offsets_pct); +} + +uint64_t TextureMappingManager::allocate_stable_id() +{ + const uint64_t stable_id = std::max(1, m_next_stable_id); + m_next_stable_id = stable_id + 1; + return stable_id; +} + +uint64_t TextureMappingManager::normalize_stable_id(uint64_t stable_id) +{ + if (stable_id == 0) + return allocate_stable_id(); + if (stable_id >= m_next_stable_id) + m_next_stable_id = stable_id + 1; + return stable_id; +} + +void TextureMappingManager::clear() +{ + m_zones.clear(); +} + +void TextureMappingManager::refresh(const std::vector &filament_colours) +{ + m_filament_colours = filament_colours; + for (TextureMappingZone &zone : m_zones) { + zone.stable_id = normalize_stable_id(zone.stable_id); + if (zone.display_color.empty() || zone.display_color[0] != '#') + zone.display_color = random_display_color(zone.stable_id); + } +} + +void TextureMappingManager::remove_physical_filament(unsigned int deleted_filament_id) +{ + if (deleted_filament_id == 0) + return; + + if (deleted_filament_id <= m_filament_colours.size()) + m_filament_colours.erase(m_filament_colours.begin() + ptrdiff_t(deleted_filament_id - 1)); + const size_t new_physical_count = m_filament_colours.size(); + + auto remap_id = [deleted_filament_id](unsigned int id) { + if (id == deleted_filament_id) + return 0u; + if (id > deleted_filament_id) + return id - 1; + return id; + }; + + for (TextureMappingZone &zone : m_zones) { + zone.component_a = remap_id(zone.component_a); + zone.component_b = remap_id(zone.component_b); + + std::vector ids = decode_component_ids(zone.component_ids, 9); + for (unsigned int &id : ids) + id = remap_id(id); + ids.erase(std::remove(ids.begin(), ids.end(), 0u), ids.end()); + if (ids.size() < 2) { + ids.clear(); + if (zone.component_a >= 1 && zone.component_a <= new_physical_count) + ids.emplace_back(zone.component_a); + if (zone.component_b >= 1 && zone.component_b <= new_physical_count && zone.component_b != zone.component_a) + ids.emplace_back(zone.component_b); + } + if (ids.size() < 2 && new_physical_count >= 2) { + ids.clear(); + for (size_t i = 1; i <= std::min(new_physical_count, 9); ++i) + ids.emplace_back(unsigned(i)); + zone.component_a = ids[0]; + zone.component_b = ids[1]; + } + if (new_physical_count < 2) + zone.enabled = false; + zone.component_ids = encode_component_ids(ids); + + auto remove_index = [deleted_filament_id](std::vector &values) { + if (deleted_filament_id >= 1 && size_t(deleted_filament_id - 1) < values.size()) + values.erase(values.begin() + ptrdiff_t(deleted_filament_id - 1)); + }; + remove_index(zone.filament_strengths_pct); + remove_index(zone.filament_minimum_offsets_pct); + } + normalize_zone_ids(new_physical_count); +} + +TextureMappingZone *TextureMappingManager::add_zone(size_t num_physical, + const std::vector &filament_colours, + int surface_pattern) +{ + if (num_physical < 2) + return nullptr; + + TextureMappingZone zone; + zone.stable_id = allocate_stable_id(); + zone.zone_id = allocate_zone_id(num_physical); + if (zone.zone_id == 0) + return nullptr; + zone.surface_pattern = surface_pattern == int(TextureMappingZone::Gradient2D) ? + int(TextureMappingZone::Gradient2D) : + int(TextureMappingZone::ImageTexture); + + std::vector ids; + for (size_t i = 1; i <= std::min(num_physical, 9); ++i) + ids.emplace_back(unsigned(i)); + zone.component_ids = encode_component_ids(ids); + zone.component_a = ids[0]; + zone.component_b = ids.size() > 1 ? ids[1] : ids[0]; + zone.display_color = random_display_color(zone.stable_id); + if (zone.is_image_texture()) + auto_adjust_texture_component_ids(zone, num_physical, filament_colours); + + m_zones.emplace_back(std::move(zone)); + refresh(filament_colours); + return &m_zones.back(); +} + +bool TextureMappingManager::duplicate_zone(size_t zone_index, + size_t num_physical, + const std::vector &filament_colours) +{ + if (zone_index >= m_zones.size() || num_physical < 2) + return false; + + TextureMappingZone copy = m_zones[zone_index]; + copy.stable_id = allocate_stable_id(); + copy.zone_id = allocate_zone_id(num_physical); + if (copy.zone_id == 0) + return false; + copy.enabled = true; + copy.deleted = false; + copy.display_color = random_display_color(copy.stable_id); + + m_zones.insert(m_zones.begin() + ptrdiff_t(zone_index + 1), std::move(copy)); + normalize_zone_ids(num_physical); + refresh(filament_colours); + return true; +} + +unsigned int TextureMappingManager::find_image_texture_zone_id(size_t) const +{ + for (const TextureMappingZone &zone : m_zones) + if (zone.enabled && !zone.deleted && zone.is_image_texture() && zone.zone_id != 0) + return zone.zone_id; + return 0; +} + +unsigned int TextureMappingManager::ensure_image_texture_zone(size_t num_physical, + const std::vector &filament_colours) +{ + if (unsigned int existing = find_image_texture_zone_id(num_physical); existing != 0) + return existing; + TextureMappingZone *zone = add_zone(num_physical, filament_colours, int(TextureMappingZone::ImageTexture)); + return zone != nullptr ? zone->zone_id : 0; +} + +std::string TextureMappingManager::serialize_entries() +{ + normalize_zone_ids(m_filament_colours.size()); + + nlohmann::json root = nlohmann::json::array(); + for (TextureMappingZone &zone : m_zones) { + if (zone.deleted) + continue; + + zone.stable_id = normalize_stable_id(zone.stable_id); + if (zone.display_color.empty() || zone.display_color[0] != '#') + zone.display_color = random_display_color(zone.stable_id); + + std::vector component_ids = decode_component_ids(zone.component_ids, 9); + if (component_ids.size() < 2) { + component_ids = {zone.component_a, zone.component_b}; + component_ids.erase(std::remove_if(component_ids.begin(), component_ids.end(), [](unsigned int id) { + return id == 0 || id > 9; + }), component_ids.end()); + } + + const std::string normalized_weights = normalize_weights(zone.component_weights, component_ids.size()); + const std::string normalized_distances = + normalize_offset_distances(zone.offset_distances, component_ids.size(), max_component_surface_offset_mm()); + const std::string normalized_angles = normalize_offset_angles(zone.offset_angles, component_ids.size()); + const std::vector normalized_strengths = normalize_strengths(zone.filament_strengths_pct); + const std::vector normalized_min_offsets = normalize_minimum_offsets(zone.filament_minimum_offsets_pct); + + nlohmann::json entry; + entry["schema"] = 2; + entry["uid"] = zone.stable_id; + entry["zone_id"] = zone.zone_id; + entry["enabled"] = zone.enabled; + entry["surface_pattern"] = zone.is_2d_gradient() ? "2d_gradient" : "image_texture"; + entry["anchor_filaments"] = {zone.component_a, zone.component_b}; + entry["component_filaments"] = ids_to_json(component_ids); + entry["component_weights_pct"] = weights_to_json(normalized_weights, component_ids.size()); + entry["display_color"] = zone.display_color; + + nlohmann::json texture; + texture["mode"] = mapping_mode_name(clamp_int(zone.texture_mapping_mode, + int(TextureMappingZone::TextureMappingFilamentBlending), + int(TextureMappingZone::TextureMappingRawValues))); + texture["color_model"] = color_model_name(zone.filament_color_mode); + texture["ordered_roles"] = zone.force_sequential_filaments; + texture["reduce_outer_surface_texture"] = zone.reduce_outer_surface_texture; + texture["hide_seams"] = zone.seam_hiding; + texture["nonlinear_offset_adjustment"] = zone.nonlinear_offset_adjustment; + texture["compact_offset_mode"] = zone.compact_offset_mode; + 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); + texture["sagging_ratio"] = normalize_sagging_ratio(zone.sagging_ratio); + texture["preview_opacity_pct"] = std::clamp(finite_or(zone.preview_opacity_pct, TextureMappingZone::DefaultPreviewOpacityPct), 0.f, 100.f); + texture["simulate_preview_colors"] = zone.preview_simulate_colors; + texture["limit_preview_resolution"] = zone.preview_limit_resolution; + texture["auto_adjust_filaments"] = zone.auto_adjust_filament_selection; + texture["strength_pct"] = floats_to_json(normalized_strengths); + texture["minimum_offset_pct"] = floats_to_json(normalized_min_offsets); + entry["texture_options"] = std::move(texture); + + nlohmann::json offset; + offset["distances_mm_by_filament"] = normalized_distances; + offset["angles_deg_by_filament"] = normalized_angles; + offset["control_mode"] = clamp_int(zone.offset_mode, + int(TextureMappingZone::OffsetBasic), + int(TextureMappingZone::OffsetAdvanced)); + offset["rotate_with_height"] = zone.offset_rotation_enabled; + offset["rotations"] = zone.offset_rotations; + offset["repeats"] = zone.offset_repeats; + offset["alternate_repeats"] = zone.offset_reverse_repeats; + offset["clockwise"] = zone.offset_clockwise; + offset["fade"] = clamp_int(zone.offset_fade_mode, + int(TextureMappingZone::OffsetFadeNone), + int(TextureMappingZone::OffsetFadeOutInReversed)); + offset["angle_reference"] = clamp_int(zone.offset_angle_mode, + int(TextureMappingZone::OffsetAngleConfigured), + int(TextureMappingZone::OffsetAngleObjectCenter)); + entry["surface_offset"] = std::move(offset); + + root.push_back(std::move(entry)); + } + + return root.empty() ? std::string() : root.dump(); +} + +void TextureMappingManager::load_entries(const std::string &serialized, + const std::vector &filament_colours) +{ + clear(); + refresh(filament_colours); + + const size_t n = filament_colours.size(); + if (serialized.empty() || n < 2) + return; + + nlohmann::json root; + try { + root = nlohmann::json::parse(serialized); + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(warning) << "TextureMappingManager::load_entries JSON parse failed: " << e.what(); + return; + } + if (!root.is_array()) + return; + + std::unordered_set used_stable_ids; + size_t loaded_rows = 0; + size_t skipped_rows = 0; + + auto dedupe_stable_id = [this, &used_stable_ids](uint64_t stable_id) { + stable_id = normalize_stable_id(stable_id); + if (used_stable_ids.insert(stable_id).second) + return stable_id; + uint64_t replacement = allocate_stable_id(); + used_stable_ids.insert(replacement); + return replacement; + }; + + for (const nlohmann::json &entry : root) { + if (!entry.is_object()) { + ++skipped_rows; + continue; + } + + std::vector component_ids = ids_from_json(entry.value("component_filaments", nlohmann::json::array()), n); + if (component_ids.size() < 2) { + component_ids.clear(); + for (size_t i = 1; i <= std::min(n, 9); ++i) + component_ids.emplace_back(unsigned(i)); + } + if (component_ids.size() < 2) { + ++skipped_rows; + continue; + } + + std::vector anchors = ids_from_json(entry.value("anchor_filaments", nlohmann::json::array()), n); + if (anchors.size() < 2) + anchors = {component_ids[0], component_ids[1]}; + if (anchors[0] == anchors[1]) + anchors[1] = anchors[0] == 1 ? 2 : 1; + + TextureMappingZone zone; + zone.component_a = anchors[0]; + zone.component_b = anchors[1]; + zone.stable_id = dedupe_stable_id(entry.value("uid", uint64_t(0))); + zone.zone_id = entry.value("zone_id", entry.value("filament_id", 0u)); + zone.enabled = entry.value("enabled", true); + zone.deleted = false; + zone.surface_pattern = + entry.value("surface_pattern", std::string("image_texture")) == "2d_gradient" || + entry.value("surface_pattern", std::string("image_texture")) == "surface_gradient" ? + int(TextureMappingZone::Gradient2D) : + int(TextureMappingZone::ImageTexture); + zone.component_ids = encode_component_ids(component_ids); + zone.component_weights = + weights_from_json(entry.value("component_weights_pct", nlohmann::json::array()), component_ids.size()); + zone.display_color = entry.value("display_color", std::string()); + if (zone.display_color.empty() || zone.display_color[0] != '#') + zone.display_color = random_display_color(zone.stable_id); + + const nlohmann::json texture = entry.value("texture_options", nlohmann::json::object()); + zone.texture_mapping_mode = mapping_mode_from_name(texture.value("mode", std::string("target_color"))); + zone.filament_color_mode = color_model_from_name(texture.value("color_model", std::string("cmyk"))); + zone.force_sequential_filaments = texture.value("ordered_roles", false); + zone.reduce_outer_surface_texture = texture.value("reduce_outer_surface_texture", false); + zone.seam_hiding = texture.value("hide_seams", false); + zone.nonlinear_offset_adjustment = texture.value("nonlinear_offset_adjustment", false); + zone.compact_offset_mode = texture.value("compact_offset_mode", false); + 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)); + zone.sagging_ratio = normalize_sagging_ratio(texture.value("sagging_ratio", 0.f)); + zone.preview_opacity_pct = std::clamp(texture.value("preview_opacity_pct", TextureMappingZone::DefaultPreviewOpacityPct), 0.f, 100.f); + zone.preview_simulate_colors = texture.value("simulate_preview_colors", false); + zone.preview_limit_resolution = texture.value("limit_preview_resolution", true); + zone.auto_adjust_filament_selection = texture.value("auto_adjust_filaments", true); + zone.filament_strengths_pct = normalize_strengths(floats_from_json(texture.value("strength_pct", nlohmann::json::array()))); + zone.filament_minimum_offsets_pct = normalize_minimum_offsets(floats_from_json(texture.value("minimum_offset_pct", nlohmann::json::array()))); + + const nlohmann::json offset = entry.value("surface_offset", nlohmann::json::object()); + zone.offset_distances = + normalize_offset_distances(offset.value("distances_mm_by_filament", std::string()), + component_ids.size(), + max_component_surface_offset_mm()); + zone.offset_angles = + normalize_offset_angles(offset.value("angles_deg_by_filament", std::string()), component_ids.size()); + zone.offset_mode = clamp_int(offset.value("control_mode", int(TextureMappingZone::OffsetBasic)), + int(TextureMappingZone::OffsetBasic), + int(TextureMappingZone::OffsetAdvanced)); + zone.offset_rotation_enabled = offset.value("rotate_with_height", true); + zone.offset_rotations = offset.value("rotations", 1.f); + zone.offset_repeats = offset.value("repeats", 1.f); + zone.offset_reverse_repeats = offset.value("alternate_repeats", true); + zone.offset_clockwise = offset.value("clockwise", true); + zone.offset_fade_mode = clamp_int(offset.value("fade", int(TextureMappingZone::OffsetFadeNone)), + int(TextureMappingZone::OffsetFadeNone), + int(TextureMappingZone::OffsetFadeOutInReversed)); + zone.offset_angle_mode = clamp_int(offset.value("angle_reference", int(TextureMappingZone::OffsetAngleObjectCenter)), + int(TextureMappingZone::OffsetAngleConfigured), + int(TextureMappingZone::OffsetAngleObjectCenter)); + + m_zones.emplace_back(std::move(zone)); + ++loaded_rows; + } + + normalize_zone_ids(n); + refresh(filament_colours); + BOOST_LOG_TRIVIAL(info) << "TextureMappingManager::load_entries" + << ", physical_count=" << n + << ", loaded_rows=" << loaded_rows + << ", skipped_rows=" << skipped_rows; +} + +int TextureMappingManager::zone_index_from_id(unsigned int zone_id) const +{ + for (size_t i = 0; i < m_zones.size(); ++i) { + const TextureMappingZone &zone = m_zones[i]; + if (zone.enabled && !zone.deleted && zone.zone_id == zone_id) + return int(i); + } + return -1; +} + +unsigned int TextureMappingManager::zone_id_for_index(size_t zone_index) const +{ + if (zone_index >= m_zones.size()) + return 0; + const TextureMappingZone &zone = m_zones[zone_index]; + return zone.enabled && !zone.deleted ? zone.zone_id : 0; +} + +std::vector TextureMappingManager::zone_ids_by_index() const +{ + std::vector ids(m_zones.size(), 0); + for (size_t i = 0; i < m_zones.size(); ++i) + ids[i] = zone_id_for_index(i); + return ids; +} + +unsigned int TextureMappingManager::allocate_zone_id(size_t num_physical) const +{ + const unsigned int start = std::max(TextureMappingZoneIdBase, unsigned(num_physical + 1)); + std::vector used(MaxTextureMappingZoneId + 1, false); + for (const TextureMappingZone &zone : m_zones) + if (!zone.deleted && zone.zone_id <= MaxTextureMappingZoneId) + used[zone.zone_id] = true; + + for (unsigned int id = start; id <= MaxTextureMappingZoneId; ++id) + if (!used[id]) + return id; + return 0; +} + +void TextureMappingManager::normalize_zone_ids(size_t num_physical) +{ + std::vector used(MaxTextureMappingZoneId + 1, false); + const unsigned int minimum = std::max(TextureMappingZoneIdBase, unsigned(num_physical + 1)); + + auto reserve = [&](unsigned int requested) { + if (requested >= minimum && requested <= MaxTextureMappingZoneId && !used[requested]) { + used[requested] = true; + return requested; + } + for (unsigned int id = minimum; id <= MaxTextureMappingZoneId; ++id) { + if (used[id]) + continue; + used[id] = true; + return id; + } + return 0u; + }; + + for (TextureMappingZone &zone : m_zones) { + if (!zone.enabled || zone.deleted) { + zone.zone_id = 0; + continue; + } + zone.zone_id = reserve(zone.zone_id); + } +} + +const TextureMappingZone *TextureMappingManager::zone_from_id(unsigned int zone_id) const +{ + const int idx = zone_index_from_id(zone_id); + return idx >= 0 ? &m_zones[size_t(idx)] : nullptr; +} + +TextureMappingZone *TextureMappingManager::zone_from_id(unsigned int zone_id) +{ + const int idx = zone_index_from_id(zone_id); + return idx >= 0 ? &m_zones[size_t(idx)] : nullptr; +} + +unsigned int TextureMappingManager::resolve_zone_component(unsigned int zone_id, size_t num_physical, int layer_index) const +{ + const TextureMappingZone *zone = zone_from_id(zone_id); + return zone == nullptr ? zone_id : resolve_zone_component(*zone, num_physical, m_filament_colours, layer_index); +} + +size_t TextureMappingManager::total_filaments(size_t num_physical) const +{ + size_t total = num_physical; + for (const TextureMappingZone &zone : m_zones) + if (zone.enabled && !zone.deleted) + total = std::max(total, size_t(zone.zone_id)); + return total; +} + +std::vector TextureMappingManager::display_colors(size_t num_physical) const +{ + const size_t total = total_filaments(num_physical); + if (total <= num_physical) + return {}; + + std::vector colors(total - num_physical, TextureMappingGapDisplayColor); + for (const TextureMappingZone &zone : m_zones) { + if (!zone.enabled || zone.deleted || zone.zone_id <= num_physical) + continue; + const size_t idx = size_t(zone.zone_id - unsigned(num_physical) - 1); + if (idx < colors.size()) + colors[idx] = zone.display_color.empty() ? TextureMappingGapDisplayColor : zone.display_color; + } + return colors; +} + +std::string TextureMappingManager::filament_color_mode_name(int filament_color_mode) +{ + return color_model_name(filament_color_mode); +} + +size_t TextureMappingManager::expected_component_count(int mapping_mode, int filament_color_mode) +{ + const int clamped_mapping = clamp_int(mapping_mode, + int(TextureMappingZone::TextureMappingFilamentBlending), + int(TextureMappingZone::TextureMappingRawValues)); + if (clamped_mapping == int(TextureMappingZone::TextureMappingRawValues)) + return 0; + + switch (clamp_int(filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW))) { + case int(TextureMappingZone::FilamentColorRGB): + case int(TextureMappingZone::FilamentColorCMY): + return 3; + case int(TextureMappingZone::FilamentColorCMYK): + case int(TextureMappingZone::FilamentColorCMYW): + case int(TextureMappingZone::FilamentColorRGBK): + case int(TextureMappingZone::FilamentColorRGBW): + return 4; + case int(TextureMappingZone::FilamentColorBW): + return 2; + default: + return 0; + } +} + +bool TextureMappingManager::component_count_mismatch(const TextureMappingZone &zone, size_t num_physical) +{ + const size_t expected = expected_component_count(zone.texture_mapping_mode, zone.filament_color_mode); + return expected != 0 && selected_component_ids(zone, num_physical).size() != expected; +} + +std::vector TextureMappingManager::selected_component_ids(const TextureMappingZone &zone, size_t num_physical) +{ + std::vector ids = decode_component_ids(zone.component_ids, num_physical); + if (!ids.empty()) + return ids; + + if (zone.component_a >= 1 && zone.component_a <= num_physical) + ids.emplace_back(zone.component_a); + if (zone.component_b >= 1 && zone.component_b <= num_physical && zone.component_b != zone.component_a) + ids.emplace_back(zone.component_b); + return ids; +} + +std::vector TextureMappingManager::effective_texture_component_ids(const TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours) +{ + std::vector selected = selected_component_ids(zone, num_physical); + const size_t expected = expected_component_count(zone.texture_mapping_mode, zone.filament_color_mode); + if (expected == 0) + return selected; + + const std::vector roles = semantic_colors(zone.filament_color_mode); + if (roles.size() != expected) + return selected; + + std::vector result; + result.reserve(expected); + std::vector used(num_physical + 1, false); + + auto choose_unused_physical = [&](const RGB &target) { + unsigned int best_id = 0; + float best_distance = std::numeric_limits::max(); + for (unsigned int id = 1; id <= num_physical; ++id) { + if (id < used.size() && used[id]) + continue; + const float distance = color_distance_sq(filament_color(id, filament_colours), target); + if (distance < best_distance) { + best_distance = distance; + best_id = id; + } + } + if (best_id != 0 && best_id < used.size()) + used[best_id] = true; + return best_id; + }; + + if (zone.force_sequential_filaments) { + for (const unsigned int id : selected) { + if (result.size() >= expected) + break; + if (id >= 1 && id <= num_physical && id < used.size() && !used[id]) { + used[id] = true; + result.emplace_back(id); + } + } + for (size_t role_idx = result.size(); role_idx < expected; ++role_idx) { + const unsigned int id = choose_unused_physical(roles[role_idx]); + if (id != 0) + result.emplace_back(id); + } + return result; + } + + std::vector selected_used(selected.size(), false); + for (const RGB &role : roles) { + size_t best_selected = selected.size(); + float best_distance = std::numeric_limits::max(); + for (size_t i = 0; i < selected.size(); ++i) { + const unsigned int id = selected[i]; + if (selected_used[i] || id < 1 || id > num_physical) + continue; + const float distance = color_distance_sq(filament_color(id, filament_colours), role); + if (distance < best_distance) { + best_distance = distance; + best_selected = i; + } + } + + unsigned int id = 0; + if (best_selected < selected.size()) { + id = selected[best_selected]; + selected_used[best_selected] = true; + if (id < used.size()) + used[id] = true; + } else { + id = choose_unused_physical(role); + } + if (id != 0) + result.emplace_back(id); + } + + return result; +} + +bool TextureMappingManager::auto_adjust_texture_component_ids(TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours) +{ + if (!zone.enabled || zone.deleted || !zone.is_image_texture() || !zone.auto_adjust_filament_selection || num_physical < 2) + return false; + + const size_t expected = expected_component_count(zone.texture_mapping_mode, zone.filament_color_mode); + if (expected == 0) + return false; + + const std::vector adjusted = effective_texture_component_ids(zone, num_physical, filament_colours); + if (adjusted.size() < 2) + return false; + + const std::string encoded = encode_component_ids(adjusted); + if (encoded.empty()) + return false; + + const unsigned int component_a = adjusted[0]; + const unsigned int component_b = adjusted.size() > 1 ? adjusted[1] : adjusted[0]; + if (zone.component_ids == encoded && zone.component_a == component_a && zone.component_b == component_b) + return false; + + zone.component_a = component_a; + zone.component_b = component_b; + zone.component_ids = encoded; + zone.component_weights = normalize_weights(zone.component_weights, adjusted.size()); + return true; +} + +float TextureMappingManager::max_component_surface_offset_mm(float reference_width_mm) +{ + const float safe_reference = std::max(0.05f, std::abs(reference_width_mm)); + return std::clamp(safe_reference, 0.01f, 0.35f); +} + +std::vector TextureMappingManager::default_offset_distances(size_t component_count, float reference_width_mm) +{ + return std::vector(component_count, max_component_surface_offset_mm(reference_width_mm)); +} + +std::vector TextureMappingManager::default_offset_angles(size_t component_count) +{ + std::vector angles(component_count, 0.f); + for (size_t i = 0; i < component_count; ++i) + angles[i] = (360.f * float(i)) / std::max(1.f, float(component_count)); + return angles; +} + +std::vector TextureMappingManager::effective_offset_distances(const TextureMappingZone &zone, + size_t component_count, + float reference_width_mm) +{ + const float max_distance = max_component_surface_offset_mm(reference_width_mm); + std::vector distances = decode_offset_distances(zone.offset_distances, component_count, max_distance); + return distances.size() == component_count ? distances : default_offset_distances(component_count, reference_width_mm); +} + +std::vector TextureMappingManager::effective_offset_angles(const TextureMappingZone &zone, size_t component_count) +{ + std::vector angles = decode_offset_angles(zone.offset_angles, component_count); + return angles.size() == component_count ? angles : default_offset_angles(component_count); +} + +unsigned int TextureMappingManager::resolve_zone_component(const TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours, + int layer_index) +{ + if (!zone.enabled || zone.deleted || num_physical == 0) + return 0; + + std::vector component_ids = zone.is_image_texture() ? + effective_texture_component_ids(zone, num_physical, filament_colours) : + selected_component_ids(zone, num_physical); + component_ids.erase(std::remove_if(component_ids.begin(), + component_ids.end(), + [num_physical](unsigned int id) { return id == 0 || id > num_physical; }), + component_ids.end()); + component_ids.erase(std::unique(component_ids.begin(), component_ids.end()), component_ids.end()); + if (component_ids.empty()) + return 0; + + const std::vector weights = decode_weights(zone.component_weights, component_ids.size()); + const std::vector sequence = + build_balanced_component_sequence(component_ids, + weights.empty() ? std::vector(component_ids.size(), 1) : weights); + if (sequence.empty()) + return component_ids.front(); + + return sequence[size_t(safe_mod(layer_index, int(sequence.size())))]; +} + +std::string TextureMappingManager::blend_color_multi(const std::vector> &color_percents) +{ + if (color_percents.empty()) + return "#000000"; + + struct WeightedColor { + RGB color; + int pct = 0; + }; + + std::vector colors; + int total_pct = 0; + for (const auto &[hex, pct] : color_percents) { + if (pct <= 0) + continue; + colors.push_back({parse_hex_color(hex), pct}); + total_pct += pct; + } + if (colors.empty() || total_pct <= 0) + return "#000000"; + + unsigned char r = static_cast(colors.front().color.r); + unsigned char g = static_cast(colors.front().color.g); + unsigned char b = static_cast(colors.front().color.b); + int accumulated = colors.front().pct; + + for (size_t i = 1; i < colors.size(); ++i) { + const int new_total = accumulated + colors[i].pct; + if (new_total <= 0) + continue; + const float t = float(colors[i].pct) / float(new_total); + filament_mixer_lerp(r, g, b, + static_cast(colors[i].color.r), + static_cast(colors[i].color.g), + static_cast(colors[i].color.b), + t, &r, &g, &b); + accumulated = new_total; + } + + return rgb_to_hex({int(r), int(g), int(b)}); +} + +} // namespace Slic3r diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp new file mode 100644 index 0000000000..0572a50802 --- /dev/null +++ b/src/libslic3r/TextureMapping.hpp @@ -0,0 +1,254 @@ +#ifndef slic3r_TextureMapping_hpp_ +#define slic3r_TextureMapping_hpp_ + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +struct TextureMappingZone +{ + static constexpr float DefaultPreviewOpacityPct = 100.f; + + enum SurfacePattern : uint8_t { + ImageTexture = 0, + Gradient2D = 1 + }; + + enum OffsetControlMode : uint8_t { + OffsetBasic = 0, + OffsetAdvanced = 1 + }; + + enum OffsetFadeMode : uint8_t { + OffsetFadeNone = 0, + OffsetFadeInUp, + OffsetFadeOutUp, + OffsetFadeInOut, + OffsetFadeOutIn, + OffsetFadeOutInReversed + }; + + enum OffsetAngleMode : uint8_t { + OffsetAngleConfigured = 0, + OffsetAngleSurfaceNormal = 1, + OffsetAngleObjectCenter = 2 + }; + + enum TextureMappingMode : uint8_t { + TextureMappingFilamentBlending = 0, + TextureMappingRawValues = 1 + }; + + enum FilamentColorMode : uint8_t { + FilamentColorAny = 0, + FilamentColorRGB = 1, + FilamentColorCMY = 2, + FilamentColorCMYK = 3, + FilamentColorCMYW = 4, + FilamentColorRGBK = 5, + FilamentColorRGBW = 6, + FilamentColorBW = 7 + }; + + static constexpr int DefaultSurfacePattern = int(ImageTexture); + static constexpr int DefaultOffsetMode = int(OffsetBasic); + static constexpr bool DefaultOffsetRotationEnabled = true; + static constexpr float DefaultOffsetRotations = 1.f; + static constexpr float DefaultOffsetRepeats = 1.f; + static constexpr bool DefaultOffsetReverseRepeats = true; + static constexpr bool DefaultOffsetClockwise = true; + static constexpr int DefaultOffsetFadeMode = int(OffsetFadeNone); + static constexpr int DefaultOffsetAngleMode = int(OffsetAngleObjectCenter); + static constexpr int DefaultTextureMappingMode = int(TextureMappingFilamentBlending); + static constexpr int DefaultFilamentColorMode = int(FilamentColorCMYK); + static constexpr bool DefaultForceSequentialFilaments = false; + static constexpr bool DefaultReduceOuterSurfaceTexture = false; + static constexpr bool DefaultSeamHiding = false; + static constexpr bool DefaultNonlinearOffsetAdjustment = false; + static constexpr bool DefaultCompactOffsetMode = false; + static constexpr float DefaultContrastPct = 100.f; + static constexpr bool DefaultHighResolutionSampling = true; + static constexpr float DefaultToneGamma = 1.f; + static constexpr float DefaultSaggingRatio = 0.f; + static constexpr bool DefaultPreviewSimulateColors = false; + static constexpr bool DefaultPreviewLimitResolution = true; + static constexpr bool DefaultAutoAdjustFilamentSelection = true; + + uint64_t stable_id = 0; + unsigned int zone_id = 0; + bool enabled = true; + bool deleted = false; + int surface_pattern = DefaultSurfacePattern; + unsigned int component_a = 1; + unsigned int component_b = 2; + std::string component_ids; + std::string component_weights; + std::string display_color; + + std::string offset_distances; + std::string offset_angles; + int offset_mode = DefaultOffsetMode; + bool offset_rotation_enabled = DefaultOffsetRotationEnabled; + float offset_rotations = DefaultOffsetRotations; + float offset_repeats = DefaultOffsetRepeats; + bool offset_reverse_repeats = DefaultOffsetReverseRepeats; + bool offset_clockwise = DefaultOffsetClockwise; + int offset_fade_mode = DefaultOffsetFadeMode; + int offset_angle_mode = DefaultOffsetAngleMode; + + int texture_mapping_mode = DefaultTextureMappingMode; + int filament_color_mode = DefaultFilamentColorMode; + bool force_sequential_filaments = DefaultForceSequentialFilaments; + bool reduce_outer_surface_texture = DefaultReduceOuterSurfaceTexture; + bool seam_hiding = DefaultSeamHiding; + bool nonlinear_offset_adjustment = DefaultNonlinearOffsetAdjustment; + bool compact_offset_mode = DefaultCompactOffsetMode; + float contrast_pct = DefaultContrastPct; + bool high_resolution_sampling = DefaultHighResolutionSampling; + float tone_gamma = DefaultToneGamma; + float sagging_ratio = DefaultSaggingRatio; + float preview_opacity_pct = DefaultPreviewOpacityPct; + bool preview_simulate_colors = DefaultPreviewSimulateColors; + bool preview_limit_resolution = DefaultPreviewLimitResolution; + bool auto_adjust_filament_selection = DefaultAutoAdjustFilamentSelection; + std::vector filament_strengths_pct; + std::vector filament_minimum_offsets_pct; + + bool is_image_texture() const { return surface_pattern == int(ImageTexture); } + bool is_2d_gradient() const { return surface_pattern == int(Gradient2D); } + + void reset_offset_settings() + { + offset_distances.clear(); + offset_angles.clear(); + offset_mode = DefaultOffsetMode; + offset_rotation_enabled = DefaultOffsetRotationEnabled; + offset_rotations = DefaultOffsetRotations; + offset_repeats = DefaultOffsetRepeats; + offset_reverse_repeats = DefaultOffsetReverseRepeats; + offset_clockwise = DefaultOffsetClockwise; + offset_fade_mode = DefaultOffsetFadeMode; + offset_angle_mode = DefaultOffsetAngleMode; + } + + void reset_texture_options() + { + texture_mapping_mode = DefaultTextureMappingMode; + filament_color_mode = DefaultFilamentColorMode; + force_sequential_filaments = DefaultForceSequentialFilaments; + reduce_outer_surface_texture = DefaultReduceOuterSurfaceTexture; + seam_hiding = DefaultSeamHiding; + nonlinear_offset_adjustment = DefaultNonlinearOffsetAdjustment; + compact_offset_mode = DefaultCompactOffsetMode; + contrast_pct = DefaultContrastPct; + high_resolution_sampling = DefaultHighResolutionSampling; + tone_gamma = DefaultToneGamma; + sagging_ratio = DefaultSaggingRatio; + preview_opacity_pct = DefaultPreviewOpacityPct; + preview_simulate_colors = DefaultPreviewSimulateColors; + preview_limit_resolution = DefaultPreviewLimitResolution; + auto_adjust_filament_selection = DefaultAutoAdjustFilamentSelection; + filament_strengths_pct.clear(); + filament_minimum_offsets_pct.clear(); + } + + bool has_custom_offset_settings() const + { + constexpr float eps = 1e-6f; + return !offset_distances.empty() || + !offset_angles.empty() || + offset_mode != DefaultOffsetMode || + offset_rotation_enabled != DefaultOffsetRotationEnabled || + std::abs(offset_rotations - DefaultOffsetRotations) > eps || + std::abs(offset_repeats - DefaultOffsetRepeats) > eps || + offset_reverse_repeats != DefaultOffsetReverseRepeats || + offset_clockwise != DefaultOffsetClockwise || + offset_fade_mode != DefaultOffsetFadeMode || + offset_angle_mode != DefaultOffsetAngleMode; + } + + bool operator==(const TextureMappingZone &rhs) const; + bool operator!=(const TextureMappingZone &rhs) const { return !(*this == rhs); } +}; + +class TextureMappingManager +{ +public: + TextureMappingManager() = default; + + void clear(); + void refresh(const std::vector &filament_colours); + void remove_physical_filament(unsigned int deleted_filament_id); + + TextureMappingZone *add_zone(size_t num_physical, + const std::vector &filament_colours, + int surface_pattern = int(TextureMappingZone::ImageTexture)); + bool duplicate_zone(size_t zone_index, + size_t num_physical, + const std::vector &filament_colours); + + unsigned int find_image_texture_zone_id(size_t num_physical) const; + unsigned int ensure_image_texture_zone(size_t num_physical, const std::vector &filament_colours); + + std::string serialize_entries(); + void load_entries(const std::string &serialized, const std::vector &filament_colours); + + int zone_index_from_id(unsigned int zone_id) const; + unsigned int zone_id_for_index(size_t zone_index) const; + std::vector zone_ids_by_index() const; + unsigned int allocate_zone_id(size_t num_physical) const; + void normalize_zone_ids(size_t num_physical); + const TextureMappingZone *zone_from_id(unsigned int zone_id) const; + TextureMappingZone *zone_from_id(unsigned int zone_id); + bool is_texture_mapping_zone_id(unsigned int zone_id) const { return zone_from_id(zone_id) != nullptr; } + unsigned int resolve_zone_component(unsigned int zone_id, size_t num_physical, int layer_index) const; + + size_t total_filaments(size_t num_physical) const; + std::vector display_colors(size_t num_physical) const; + std::vector display_colors() const { return display_colors(m_filament_colours.size()); } + + static std::string filament_color_mode_name(int filament_color_mode); + static size_t expected_component_count(int mapping_mode, int filament_color_mode); + static bool component_count_mismatch(const TextureMappingZone &zone, size_t num_physical); + static std::vector effective_texture_component_ids(const TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours); + static std::vector selected_component_ids(const TextureMappingZone &zone, size_t num_physical); + static bool auto_adjust_texture_component_ids(TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours); + + static std::vector default_offset_distances(size_t component_count, float reference_width_mm = 0.4f); + static std::vector default_offset_angles(size_t component_count); + static std::vector effective_offset_distances(const TextureMappingZone &zone, + size_t component_count, + float reference_width_mm = 0.4f); + static std::vector effective_offset_angles(const TextureMappingZone &zone, size_t component_count); + static float max_component_surface_offset_mm(float reference_width_mm = 0.4f); + static unsigned int resolve_zone_component(const TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours, + int layer_index); + + static std::string blend_color_multi(const std::vector> &color_percents); + + const std::vector &zones() const { return m_zones; } + std::vector &zones() { return m_zones; } + +private: + uint64_t allocate_stable_id(); + uint64_t normalize_stable_id(uint64_t stable_id); + + std::vector m_zones; + uint64_t m_next_stable_id = 1; + std::vector m_filament_colours; +}; + +} // namespace Slic3r + +#endif /* slic3r_TextureMapping_hpp_ */ diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a6d19f505c..586246dd00 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1470,6 +1470,32 @@ void TriangleSelector::get_facets(std::vector& facets_per_ } } +void TriangleSelector::get_facet_triangles(std::vector> &facets_per_type) const +{ + facets_per_type.clear(); + + int max_state = int(EnforcerBlockerType::NONE); + for (const Triangle &tr : m_triangles) + if (tr.valid() && !tr.is_split()) + max_state = std::max(max_state, int(tr.get_state())); + + facets_per_type.resize(size_t(max_state + 1)); + for (const Triangle &tr : m_triangles) { + if (!tr.valid() || tr.is_split()) + continue; + + const int state = int(tr.get_state()); + if (state < 0 || state >= int(facets_per_type.size())) + continue; + + FacetStateTriangle facet; + facet.source_triangle = tr.source_triangle; + for (size_t idx = 0; idx < facet.vertices.size(); ++idx) + facet.vertices[idx] = m_vertices[size_t(tr.verts_idxs[idx])].v; + facets_per_type[size_t(state)].emplace_back(std::move(facet)); + } +} + indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const { indexed_triangle_set out; diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 50bbdd4ed0..4e71eaa3fb 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -287,6 +287,11 @@ public: template void serialize(Archive &ar) { ar(triangles_to_split, bitstream, used_states); } }; + struct FacetStateTriangle { + std::array vertices; + int source_triangle = -1; + }; + std::pair, std::vector> precompute_all_neighbors() const; void precompute_all_neighbors_recursive(int facet_idx, const Vec3i32 &neighbors, const Vec3i32 &neighbors_propagated, std::vector &neighbors_out, std::vector &neighbors_normal_out) const; @@ -337,6 +342,7 @@ public: // BBS void get_facets(std::vector& facets_per_type) const; + void get_facet_triangles(std::vector> &facets_per_type) const; // Set facet of the mesh to a given state. Only works for original triangles. void set_facet(int facet_idx, EnforcerBlockerType state); diff --git a/src/libslic3r/TriangleSetSampling.cpp b/src/libslic3r/TriangleSetSampling.cpp index bb03ff6d75..73e13bbd39 100644 --- a/src/libslic3r/TriangleSetSampling.cpp +++ b/src/libslic3r/TriangleSetSampling.cpp @@ -1,5 +1,6 @@ #include "TriangleSetSampling.hpp" -#include +#include +#include #include #include #include @@ -7,27 +8,53 @@ namespace Slic3r { TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const indexed_triangle_set &triangle_set) { - std::vector triangles_area(triangle_set.indices.size()); + TriangleSetSamples result; + result.total_area = 0.f; - tbb::parallel_for(tbb::blocked_range(0, triangle_set.indices.size()), - [&triangle_set, &triangles_area]( - tbb::blocked_range r) { - for (size_t t_idx = r.begin(); t_idx < r.end(); ++t_idx) { - const Vec3f &a = triangle_set.vertices[triangle_set.indices[t_idx].x()]; - const Vec3f &b = triangle_set.vertices[triangle_set.indices[t_idx].y()]; - const Vec3f &c = triangle_set.vertices[triangle_set.indices[t_idx].z()]; - double area = double(0.5 * (b - a).cross(c - a).norm()); - triangles_area[t_idx] = area; - } - }); + if (samples_count == 0 || triangle_set.indices.empty() || triangle_set.vertices.empty()) + return result; - std::map area_sum_to_triangle_idx; - float area_sum = 0; - for (size_t t_idx = 0; t_idx < triangles_area.size(); ++t_idx) { - area_sum += triangles_area[t_idx]; - area_sum_to_triangle_idx[area_sum] = t_idx; + const std::vector &indices = triangle_set.indices; + const std::vector &vertices = triangle_set.vertices; + const size_t vertex_count = vertices.size(); + + std::vector cumulative_area; + std::vector valid_triangle_indices; + cumulative_area.reserve(indices.size()); + valid_triangle_indices.reserve(indices.size()); + + double total_area = 0.0; + for (size_t t_idx = 0; t_idx < indices.size(); ++t_idx) { + const stl_triangle_vertex_indices &tri = indices[t_idx]; + if (tri.x() < 0 || tri.y() < 0 || tri.z() < 0) + continue; + + const size_t ia = size_t(tri.x()); + const size_t ib = size_t(tri.y()); + const size_t ic = size_t(tri.z()); + if (ia >= vertex_count || ib >= vertex_count || ic >= vertex_count) + continue; + + const Vec3f &a = vertices[ia]; + const Vec3f &b = vertices[ib]; + const Vec3f &c = vertices[ic]; + if (!std::isfinite(a.x()) || !std::isfinite(a.y()) || !std::isfinite(a.z()) || + !std::isfinite(b.x()) || !std::isfinite(b.y()) || !std::isfinite(b.z()) || + !std::isfinite(c.x()) || !std::isfinite(c.y()) || !std::isfinite(c.z())) + continue; + + const double area = double(0.5f * (b - a).cross(c - a).norm()); + if (!std::isfinite(area) || area <= 0.0) + continue; + + total_area += area; + cumulative_area.emplace_back(total_area); + valid_triangle_indices.emplace_back(t_idx); } + if (valid_triangle_indices.empty() || total_area <= 0.0 || !std::isfinite(total_area)) + return result; + std::mt19937_64 mersenne_engine { 27644437 }; // random numbers on interval [0, 1) std::uniform_real_distribution fdistribution; @@ -39,28 +66,37 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index std::vector random_samples(samples_count); std::generate(random_samples.begin(), random_samples.end(), get_random); - TriangleSetSamples result; - result.total_area = area_sum; + result.total_area = float(total_area); result.positions.resize(samples_count); result.normals.resize(samples_count); result.triangle_indices.resize(samples_count); tbb::parallel_for(tbb::blocked_range(0, samples_count), - [&triangle_set, &area_sum_to_triangle_idx, &area_sum, &random_samples, &result]( + [&indices, &vertices, &cumulative_area, &valid_triangle_indices, &total_area, &random_samples, &result]( tbb::blocked_range r) { for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) { - double t_sample = random_samples[s_idx].x() * area_sum; - size_t t_idx = area_sum_to_triangle_idx.upper_bound(t_sample)->second; + const double t_sample = random_samples[s_idx].x() * total_area; + const auto it = std::upper_bound(cumulative_area.begin(), cumulative_area.end(), t_sample); + const size_t sampled_idx = + (it == cumulative_area.end()) ? (cumulative_area.size() - 1) : size_t(std::distance(cumulative_area.begin(), it)); + const size_t t_idx = valid_triangle_indices[sampled_idx]; - double sq_u = std::sqrt(random_samples[s_idx].y()); - double v = random_samples[s_idx].z(); + const double sq_u = std::sqrt(random_samples[s_idx].y()); + const double v = random_samples[s_idx].z(); - Vec3f A = triangle_set.vertices[triangle_set.indices[t_idx].x()]; - Vec3f B = triangle_set.vertices[triangle_set.indices[t_idx].y()]; - Vec3f C = triangle_set.vertices[triangle_set.indices[t_idx].z()]; + const stl_triangle_vertex_indices &tri = indices[t_idx]; + const Vec3f &A = vertices[size_t(tri.x())]; + const Vec3f &B = vertices[size_t(tri.y())]; + const Vec3f &C = vertices[size_t(tri.z())]; result.positions[s_idx] = A * (1 - sq_u) + B * (sq_u * (1 - v)) + C * (v * sq_u); - result.normals[s_idx] = ((B - A).cross(C - B)).normalized(); + Vec3f normal = (B - A).cross(C - B); + const float normal_len = normal.norm(); + if (normal_len > 0.f && std::isfinite(normal_len)) + normal /= normal_len; + else + normal = Vec3f(0.f, 0.f, 1.f); + result.normals[s_idx] = normal; result.triangle_indices[s_idx] = t_idx; } }); diff --git a/src/libslic3r/filament_mixer.cpp b/src/libslic3r/filament_mixer.cpp new file mode 100644 index 0000000000..fed0b47af9 --- /dev/null +++ b/src/libslic3r/filament_mixer.cpp @@ -0,0 +1,81 @@ +#include "filament_mixer.h" + +#include +#include + +#include "filament_mixer_model.h" + +namespace Slic3r { +namespace { + +inline float clamp01(float x) +{ + return std::max(0.0f, std::min(1.0f, x)); +} + +inline float srgb_to_linear(float x) +{ + return (x >= 0.04045f) ? std::pow((x + 0.055f) / 1.055f, 2.4f) : x / 12.92f; +} + +inline float linear_to_srgb(float x) +{ + return (x >= 0.0031308f) ? (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f) : (12.92f * x); +} + +inline unsigned char to_u8(float x) +{ + const float clamped = clamp01(x); + return static_cast(clamped * 255.0f + 0.5f); +} + +inline float to_f01(unsigned char x) +{ + return static_cast(x) / 255.0f; +} + +} // namespace + +void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t, + unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) +{ + ::filament_mixer::lerp(r1, g1, b1, r2, g2, b2, t, out_r, out_g, out_b); +} + +void filament_mixer_lerp_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b) +{ + unsigned char ur = 0, ug = 0, ub = 0; + filament_mixer_lerp(to_u8(r1), to_u8(g1), to_u8(b1), + to_u8(r2), to_u8(g2), to_u8(b2), + t, &ur, &ug, &ub); + *out_r = to_f01(ur); + *out_g = to_f01(ug); + *out_b = to_f01(ub); +} + +void filament_mixer_lerp_linear_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b) +{ + const float sr1 = linear_to_srgb(clamp01(r1)); + const float sg1 = linear_to_srgb(clamp01(g1)); + const float sb1 = linear_to_srgb(clamp01(b1)); + const float sr2 = linear_to_srgb(clamp01(r2)); + const float sg2 = linear_to_srgb(clamp01(g2)); + const float sb2 = linear_to_srgb(clamp01(b2)); + + float out_sr = 0.0f, out_sg = 0.0f, out_sb = 0.0f; + filament_mixer_lerp_float(sr1, sg1, sb1, sr2, sg2, sb2, t, &out_sr, &out_sg, &out_sb); + + *out_r = srgb_to_linear(clamp01(out_sr)); + *out_g = srgb_to_linear(clamp01(out_sg)); + *out_b = srgb_to_linear(clamp01(out_sb)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/filament_mixer.h b/src/libslic3r/filament_mixer.h new file mode 100644 index 0000000000..5aa2f91fa5 --- /dev/null +++ b/src/libslic3r/filament_mixer.h @@ -0,0 +1,23 @@ +#ifndef SLIC3R_FILAMENT_MIXER_H +#define SLIC3R_FILAMENT_MIXER_H + +namespace Slic3r { + +void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t, + unsigned char* out_r, unsigned char* out_g, unsigned char* out_b); + +void filament_mixer_lerp_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b); + +void filament_mixer_lerp_linear_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/filament_mixer_model.h b/src/libslic3r/filament_mixer_model.h new file mode 100644 index 0000000000..1fddbcef8f --- /dev/null +++ b/src/libslic3r/filament_mixer_model.h @@ -0,0 +1,819 @@ +/* + * FilamentMixer — Header-only C++ pigment color mixer + * + * Filament mixer implementation using a degree-4 polynomial regression + * trained to approximate Mixbox behavior (Mean Delta-E ~2.07). + * This library does not include Mixbox source code, binaries, or data files. + * + * Usage: + * #include "filament_mixer_model.h" + * + * unsigned char r, g, b; + * filament_mixer::lerp(0, 33, 133, 252, 211, 0, 0.5f, &r, &g, &b); + * // r=47, g=141, b=56 (blue + yellow → green) + * + * No dependencies beyond the C++ standard library. + * + * MIT License + * + * Copyright (c) 2026 Justin Hayes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef FILAMENT_MIXER_H +#define FILAMENT_MIXER_H + +#include +#include +#include + +namespace filament_mixer { +namespace detail { + +// BEGIN AUTO-GENERATED COEFFICIENTS +// Auto-generated by scripts/export_poly_coefficients.py +// Do not edit manually. +// Degree-4 polynomial, 330 features, 7 inputs + +static const int POLY_DEGREE = 4; +static const int N_FEATURES = 330; +static const int N_INPUTS = 7; + +static const int POWERS[330][7] = { + {0, 0, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0, 0, 0}, + {0, 0, 0, 1, 0, 0, 0}, + {0, 0, 0, 0, 1, 0, 0}, + {0, 0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 0, 0, 1}, + {2, 0, 0, 0, 0, 0, 0}, + {1, 1, 0, 0, 0, 0, 0}, + {1, 0, 1, 0, 0, 0, 0}, + {1, 0, 0, 1, 0, 0, 0}, + {1, 0, 0, 0, 1, 0, 0}, + {1, 0, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 0, 0, 1}, + {0, 2, 0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0, 0, 0}, + {0, 1, 0, 1, 0, 0, 0}, + {0, 1, 0, 0, 1, 0, 0}, + {0, 1, 0, 0, 0, 1, 0}, + {0, 1, 0, 0, 0, 0, 1}, + {0, 0, 2, 0, 0, 0, 0}, + {0, 0, 1, 1, 0, 0, 0}, + {0, 0, 1, 0, 1, 0, 0}, + {0, 0, 1, 0, 0, 1, 0}, + {0, 0, 1, 0, 0, 0, 1}, + {0, 0, 0, 2, 0, 0, 0}, + {0, 0, 0, 1, 1, 0, 0}, + {0, 0, 0, 1, 0, 1, 0}, + {0, 0, 0, 1, 0, 0, 1}, + {0, 0, 0, 0, 2, 0, 0}, + {0, 0, 0, 0, 1, 1, 0}, + {0, 0, 0, 0, 1, 0, 1}, + {0, 0, 0, 0, 0, 2, 0}, + {0, 0, 0, 0, 0, 1, 1}, + {0, 0, 0, 0, 0, 0, 2}, + {3, 0, 0, 0, 0, 0, 0}, + {2, 1, 0, 0, 0, 0, 0}, + {2, 0, 1, 0, 0, 0, 0}, + {2, 0, 0, 1, 0, 0, 0}, + {2, 0, 0, 0, 1, 0, 0}, + {2, 0, 0, 0, 0, 1, 0}, + {2, 0, 0, 0, 0, 0, 1}, + {1, 2, 0, 0, 0, 0, 0}, + {1, 1, 1, 0, 0, 0, 0}, + {1, 1, 0, 1, 0, 0, 0}, + {1, 1, 0, 0, 1, 0, 0}, + {1, 1, 0, 0, 0, 1, 0}, + {1, 1, 0, 0, 0, 0, 1}, + {1, 0, 2, 0, 0, 0, 0}, + {1, 0, 1, 1, 0, 0, 0}, + {1, 0, 1, 0, 1, 0, 0}, + {1, 0, 1, 0, 0, 1, 0}, + {1, 0, 1, 0, 0, 0, 1}, + {1, 0, 0, 2, 0, 0, 0}, + {1, 0, 0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0, 1, 0}, + {1, 0, 0, 1, 0, 0, 1}, + {1, 0, 0, 0, 2, 0, 0}, + {1, 0, 0, 0, 1, 1, 0}, + {1, 0, 0, 0, 1, 0, 1}, + {1, 0, 0, 0, 0, 2, 0}, + {1, 0, 0, 0, 0, 1, 1}, + {1, 0, 0, 0, 0, 0, 2}, + {0, 3, 0, 0, 0, 0, 0}, + {0, 2, 1, 0, 0, 0, 0}, + {0, 2, 0, 1, 0, 0, 0}, + {0, 2, 0, 0, 1, 0, 0}, + {0, 2, 0, 0, 0, 1, 0}, + {0, 2, 0, 0, 0, 0, 1}, + {0, 1, 2, 0, 0, 0, 0}, + {0, 1, 1, 1, 0, 0, 0}, + {0, 1, 1, 0, 1, 0, 0}, + {0, 1, 1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0, 0, 1}, + {0, 1, 0, 2, 0, 0, 0}, + {0, 1, 0, 1, 1, 0, 0}, + {0, 1, 0, 1, 0, 1, 0}, + {0, 1, 0, 1, 0, 0, 1}, + {0, 1, 0, 0, 2, 0, 0}, + {0, 1, 0, 0, 1, 1, 0}, + {0, 1, 0, 0, 1, 0, 1}, + {0, 1, 0, 0, 0, 2, 0}, + {0, 1, 0, 0, 0, 1, 1}, + {0, 1, 0, 0, 0, 0, 2}, + {0, 0, 3, 0, 0, 0, 0}, + {0, 0, 2, 1, 0, 0, 0}, + {0, 0, 2, 0, 1, 0, 0}, + {0, 0, 2, 0, 0, 1, 0}, + {0, 0, 2, 0, 0, 0, 1}, + {0, 0, 1, 2, 0, 0, 0}, + {0, 0, 1, 1, 1, 0, 0}, + {0, 0, 1, 1, 0, 1, 0}, + {0, 0, 1, 1, 0, 0, 1}, + {0, 0, 1, 0, 2, 0, 0}, + {0, 0, 1, 0, 1, 1, 0}, + {0, 0, 1, 0, 1, 0, 1}, + {0, 0, 1, 0, 0, 2, 0}, + {0, 0, 1, 0, 0, 1, 1}, + {0, 0, 1, 0, 0, 0, 2}, + {0, 0, 0, 3, 0, 0, 0}, + {0, 0, 0, 2, 1, 0, 0}, + {0, 0, 0, 2, 0, 1, 0}, + {0, 0, 0, 2, 0, 0, 1}, + {0, 0, 0, 1, 2, 0, 0}, + {0, 0, 0, 1, 1, 1, 0}, + {0, 0, 0, 1, 1, 0, 1}, + {0, 0, 0, 1, 0, 2, 0}, + {0, 0, 0, 1, 0, 1, 1}, + {0, 0, 0, 1, 0, 0, 2}, + {0, 0, 0, 0, 3, 0, 0}, + {0, 0, 0, 0, 2, 1, 0}, + {0, 0, 0, 0, 2, 0, 1}, + {0, 0, 0, 0, 1, 2, 0}, + {0, 0, 0, 0, 1, 1, 1}, + {0, 0, 0, 0, 1, 0, 2}, + {0, 0, 0, 0, 0, 3, 0}, + {0, 0, 0, 0, 0, 2, 1}, + {0, 0, 0, 0, 0, 1, 2}, + {0, 0, 0, 0, 0, 0, 3}, + {4, 0, 0, 0, 0, 0, 0}, + {3, 1, 0, 0, 0, 0, 0}, + {3, 0, 1, 0, 0, 0, 0}, + {3, 0, 0, 1, 0, 0, 0}, + {3, 0, 0, 0, 1, 0, 0}, + {3, 0, 0, 0, 0, 1, 0}, + {3, 0, 0, 0, 0, 0, 1}, + {2, 2, 0, 0, 0, 0, 0}, + {2, 1, 1, 0, 0, 0, 0}, + {2, 1, 0, 1, 0, 0, 0}, + {2, 1, 0, 0, 1, 0, 0}, + {2, 1, 0, 0, 0, 1, 0}, + {2, 1, 0, 0, 0, 0, 1}, + {2, 0, 2, 0, 0, 0, 0}, + {2, 0, 1, 1, 0, 0, 0}, + {2, 0, 1, 0, 1, 0, 0}, + {2, 0, 1, 0, 0, 1, 0}, + {2, 0, 1, 0, 0, 0, 1}, + {2, 0, 0, 2, 0, 0, 0}, + {2, 0, 0, 1, 1, 0, 0}, + {2, 0, 0, 1, 0, 1, 0}, + {2, 0, 0, 1, 0, 0, 1}, + {2, 0, 0, 0, 2, 0, 0}, + {2, 0, 0, 0, 1, 1, 0}, + {2, 0, 0, 0, 1, 0, 1}, + {2, 0, 0, 0, 0, 2, 0}, + {2, 0, 0, 0, 0, 1, 1}, + {2, 0, 0, 0, 0, 0, 2}, + {1, 3, 0, 0, 0, 0, 0}, + {1, 2, 1, 0, 0, 0, 0}, + {1, 2, 0, 1, 0, 0, 0}, + {1, 2, 0, 0, 1, 0, 0}, + {1, 2, 0, 0, 0, 1, 0}, + {1, 2, 0, 0, 0, 0, 1}, + {1, 1, 2, 0, 0, 0, 0}, + {1, 1, 1, 1, 0, 0, 0}, + {1, 1, 1, 0, 1, 0, 0}, + {1, 1, 1, 0, 0, 1, 0}, + {1, 1, 1, 0, 0, 0, 1}, + {1, 1, 0, 2, 0, 0, 0}, + {1, 1, 0, 1, 1, 0, 0}, + {1, 1, 0, 1, 0, 1, 0}, + {1, 1, 0, 1, 0, 0, 1}, + {1, 1, 0, 0, 2, 0, 0}, + {1, 1, 0, 0, 1, 1, 0}, + {1, 1, 0, 0, 1, 0, 1}, + {1, 1, 0, 0, 0, 2, 0}, + {1, 1, 0, 0, 0, 1, 1}, + {1, 1, 0, 0, 0, 0, 2}, + {1, 0, 3, 0, 0, 0, 0}, + {1, 0, 2, 1, 0, 0, 0}, + {1, 0, 2, 0, 1, 0, 0}, + {1, 0, 2, 0, 0, 1, 0}, + {1, 0, 2, 0, 0, 0, 1}, + {1, 0, 1, 2, 0, 0, 0}, + {1, 0, 1, 1, 1, 0, 0}, + {1, 0, 1, 1, 0, 1, 0}, + {1, 0, 1, 1, 0, 0, 1}, + {1, 0, 1, 0, 2, 0, 0}, + {1, 0, 1, 0, 1, 1, 0}, + {1, 0, 1, 0, 1, 0, 1}, + {1, 0, 1, 0, 0, 2, 0}, + {1, 0, 1, 0, 0, 1, 1}, + {1, 0, 1, 0, 0, 0, 2}, + {1, 0, 0, 3, 0, 0, 0}, + {1, 0, 0, 2, 1, 0, 0}, + {1, 0, 0, 2, 0, 1, 0}, + {1, 0, 0, 2, 0, 0, 1}, + {1, 0, 0, 1, 2, 0, 0}, + {1, 0, 0, 1, 1, 1, 0}, + {1, 0, 0, 1, 1, 0, 1}, + {1, 0, 0, 1, 0, 2, 0}, + {1, 0, 0, 1, 0, 1, 1}, + {1, 0, 0, 1, 0, 0, 2}, + {1, 0, 0, 0, 3, 0, 0}, + {1, 0, 0, 0, 2, 1, 0}, + {1, 0, 0, 0, 2, 0, 1}, + {1, 0, 0, 0, 1, 2, 0}, + {1, 0, 0, 0, 1, 1, 1}, + {1, 0, 0, 0, 1, 0, 2}, + {1, 0, 0, 0, 0, 3, 0}, + {1, 0, 0, 0, 0, 2, 1}, + {1, 0, 0, 0, 0, 1, 2}, + {1, 0, 0, 0, 0, 0, 3}, + {0, 4, 0, 0, 0, 0, 0}, + {0, 3, 1, 0, 0, 0, 0}, + {0, 3, 0, 1, 0, 0, 0}, + {0, 3, 0, 0, 1, 0, 0}, + {0, 3, 0, 0, 0, 1, 0}, + {0, 3, 0, 0, 0, 0, 1}, + {0, 2, 2, 0, 0, 0, 0}, + {0, 2, 1, 1, 0, 0, 0}, + {0, 2, 1, 0, 1, 0, 0}, + {0, 2, 1, 0, 0, 1, 0}, + {0, 2, 1, 0, 0, 0, 1}, + {0, 2, 0, 2, 0, 0, 0}, + {0, 2, 0, 1, 1, 0, 0}, + {0, 2, 0, 1, 0, 1, 0}, + {0, 2, 0, 1, 0, 0, 1}, + {0, 2, 0, 0, 2, 0, 0}, + {0, 2, 0, 0, 1, 1, 0}, + {0, 2, 0, 0, 1, 0, 1}, + {0, 2, 0, 0, 0, 2, 0}, + {0, 2, 0, 0, 0, 1, 1}, + {0, 2, 0, 0, 0, 0, 2}, + {0, 1, 3, 0, 0, 0, 0}, + {0, 1, 2, 1, 0, 0, 0}, + {0, 1, 2, 0, 1, 0, 0}, + {0, 1, 2, 0, 0, 1, 0}, + {0, 1, 2, 0, 0, 0, 1}, + {0, 1, 1, 2, 0, 0, 0}, + {0, 1, 1, 1, 1, 0, 0}, + {0, 1, 1, 1, 0, 1, 0}, + {0, 1, 1, 1, 0, 0, 1}, + {0, 1, 1, 0, 2, 0, 0}, + {0, 1, 1, 0, 1, 1, 0}, + {0, 1, 1, 0, 1, 0, 1}, + {0, 1, 1, 0, 0, 2, 0}, + {0, 1, 1, 0, 0, 1, 1}, + {0, 1, 1, 0, 0, 0, 2}, + {0, 1, 0, 3, 0, 0, 0}, + {0, 1, 0, 2, 1, 0, 0}, + {0, 1, 0, 2, 0, 1, 0}, + {0, 1, 0, 2, 0, 0, 1}, + {0, 1, 0, 1, 2, 0, 0}, + {0, 1, 0, 1, 1, 1, 0}, + {0, 1, 0, 1, 1, 0, 1}, + {0, 1, 0, 1, 0, 2, 0}, + {0, 1, 0, 1, 0, 1, 1}, + {0, 1, 0, 1, 0, 0, 2}, + {0, 1, 0, 0, 3, 0, 0}, + {0, 1, 0, 0, 2, 1, 0}, + {0, 1, 0, 0, 2, 0, 1}, + {0, 1, 0, 0, 1, 2, 0}, + {0, 1, 0, 0, 1, 1, 1}, + {0, 1, 0, 0, 1, 0, 2}, + {0, 1, 0, 0, 0, 3, 0}, + {0, 1, 0, 0, 0, 2, 1}, + {0, 1, 0, 0, 0, 1, 2}, + {0, 1, 0, 0, 0, 0, 3}, + {0, 0, 4, 0, 0, 0, 0}, + {0, 0, 3, 1, 0, 0, 0}, + {0, 0, 3, 0, 1, 0, 0}, + {0, 0, 3, 0, 0, 1, 0}, + {0, 0, 3, 0, 0, 0, 1}, + {0, 0, 2, 2, 0, 0, 0}, + {0, 0, 2, 1, 1, 0, 0}, + {0, 0, 2, 1, 0, 1, 0}, + {0, 0, 2, 1, 0, 0, 1}, + {0, 0, 2, 0, 2, 0, 0}, + {0, 0, 2, 0, 1, 1, 0}, + {0, 0, 2, 0, 1, 0, 1}, + {0, 0, 2, 0, 0, 2, 0}, + {0, 0, 2, 0, 0, 1, 1}, + {0, 0, 2, 0, 0, 0, 2}, + {0, 0, 1, 3, 0, 0, 0}, + {0, 0, 1, 2, 1, 0, 0}, + {0, 0, 1, 2, 0, 1, 0}, + {0, 0, 1, 2, 0, 0, 1}, + {0, 0, 1, 1, 2, 0, 0}, + {0, 0, 1, 1, 1, 1, 0}, + {0, 0, 1, 1, 1, 0, 1}, + {0, 0, 1, 1, 0, 2, 0}, + {0, 0, 1, 1, 0, 1, 1}, + {0, 0, 1, 1, 0, 0, 2}, + {0, 0, 1, 0, 3, 0, 0}, + {0, 0, 1, 0, 2, 1, 0}, + {0, 0, 1, 0, 2, 0, 1}, + {0, 0, 1, 0, 1, 2, 0}, + {0, 0, 1, 0, 1, 1, 1}, + {0, 0, 1, 0, 1, 0, 2}, + {0, 0, 1, 0, 0, 3, 0}, + {0, 0, 1, 0, 0, 2, 1}, + {0, 0, 1, 0, 0, 1, 2}, + {0, 0, 1, 0, 0, 0, 3}, + {0, 0, 0, 4, 0, 0, 0}, + {0, 0, 0, 3, 1, 0, 0}, + {0, 0, 0, 3, 0, 1, 0}, + {0, 0, 0, 3, 0, 0, 1}, + {0, 0, 0, 2, 2, 0, 0}, + {0, 0, 0, 2, 1, 1, 0}, + {0, 0, 0, 2, 1, 0, 1}, + {0, 0, 0, 2, 0, 2, 0}, + {0, 0, 0, 2, 0, 1, 1}, + {0, 0, 0, 2, 0, 0, 2}, + {0, 0, 0, 1, 3, 0, 0}, + {0, 0, 0, 1, 2, 1, 0}, + {0, 0, 0, 1, 2, 0, 1}, + {0, 0, 0, 1, 1, 2, 0}, + {0, 0, 0, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 0, 2}, + {0, 0, 0, 1, 0, 3, 0}, + {0, 0, 0, 1, 0, 2, 1}, + {0, 0, 0, 1, 0, 1, 2}, + {0, 0, 0, 1, 0, 0, 3}, + {0, 0, 0, 0, 4, 0, 0}, + {0, 0, 0, 0, 3, 1, 0}, + {0, 0, 0, 0, 3, 0, 1}, + {0, 0, 0, 0, 2, 2, 0}, + {0, 0, 0, 0, 2, 1, 1}, + {0, 0, 0, 0, 2, 0, 2}, + {0, 0, 0, 0, 1, 3, 0}, + {0, 0, 0, 0, 1, 2, 1}, + {0, 0, 0, 0, 1, 1, 2}, + {0, 0, 0, 0, 1, 0, 3}, + {0, 0, 0, 0, 0, 4, 0}, + {0, 0, 0, 0, 0, 3, 1}, + {0, 0, 0, 0, 0, 2, 2}, + {0, 0, 0, 0, 0, 1, 3}, + {0, 0, 0, 0, 0, 0, 4} +}; + +static const double COEF[330][3] = { + {8.70954844857314666e-12, 1.27926950848359881e-09, -2.06865474316332923e-09}, + {1.05783308354771544e+00, -8.02119209663359686e-03, -7.88705651445470723e-02}, + {1.35905954452774837e-02, 8.71267975138422468e-01, 1.04898760410704936e-01}, + {-4.16452026099768252e-02, 1.75465381596434100e-02, 1.00224594702931546e+00}, + {4.50321316661211821e-02, -7.11409155427628892e-02, 3.91232300778902690e-03}, + {1.76675507851922452e-02, -1.32709276116036640e-01, 6.36935270589509828e-02}, + {-5.23434830565911030e-02, 3.77681739012521722e-02, -2.08691145087504179e-02}, + {-2.33722556520224792e-03, -1.57542611462692145e-03, -3.05158628452478807e-03}, + {-8.87678609044812990e-04, 3.83194388837734693e-04, 1.37779212442523083e-03}, + {-2.11519042076831979e-03, 5.82337362515735358e-04, 2.24055108941204821e-04}, + {4.61545125563611917e-04, 7.72869451707915893e-04, -1.10800630143346882e-03}, + {1.05937484157345879e-03, -3.14448681732842211e-04, -1.75129182446198098e-03}, + {1.49045689016363055e-03, -2.09220860101674106e-04, 5.93100338908187697e-04}, + {-3.51246656293852696e-04, -8.20743017485394289e-04, 5.71854064480802862e-04}, + {-9.18204643629581319e-01, -2.27788122702773155e-01, 6.39980793022790623e-02}, + {9.24243491377523679e-05, 7.32841332381495400e-04, -1.55219718415109450e-03}, + {7.13695056804217989e-04, -8.46467621879685712e-05, 6.50202947442505750e-04}, + {1.66640864747485983e-03, -1.24492362771216523e-04, 2.68236502346156410e-04}, + {-7.20253644860527516e-04, 7.81434220384157334e-04, 1.12661089007361367e-03}, + {-6.83033334365238206e-05, 7.27742627159490762e-04, -1.78048843835204584e-03}, + {-3.13431571993316588e-02, -8.57604034845650287e-01, -2.57225920656276863e-01}, + {-6.47867200595898341e-05, -1.16688982572457655e-03, 1.14174511750260031e-03}, + {-5.00713925613324338e-04, -6.87598082111323477e-04, 6.20598069880440176e-04}, + {-8.56716727659588957e-05, 9.74478786593559361e-04, -1.65892838405139512e-03}, + {6.53468478750158263e-04, 7.51662000672516676e-04, -6.73196326298856570e-04}, + {-4.42539011000103941e-02, -2.01965359697350230e-02, -9.94663493761314355e-01}, + {-7.39107395392403087e-04, 5.28870828612476996e-04, 1.00947183860234540e-03}, + {-2.06577300933763214e-03, 9.60215813758718011e-04, -3.27993888180819421e-04}, + {3.47783280638377555e-04, 8.41824316850705743e-04, -8.87458944147930993e-04}, + {1.20960551709587905e+00, -7.07660818059813873e-02, -8.56332806008946491e-03}, + {2.11116509318935269e-04, 7.68490846994171776e-04, -1.63228995491542417e-03}, + {6.47698075356516103e-04, -4.20589129268072884e-04, 1.18354001300614896e-03}, + {-2.78795945253848716e-02, 1.22199201000304547e+00, -2.07383075858847743e-01}, + {-5.32457386680677347e-05, -9.58027320315790677e-04, 9.89667309649038679e-04}, + {-9.03932426306289782e-02, -4.00969232187064692e-02, 1.26285611182120072e+00}, + {-2.19453630740322871e-03, -1.21893190049422620e-03, -1.92293368093085417e-03}, + {1.72950845415964505e-06, -8.93952511560151819e-09, -6.14874900641340649e-06}, + {8.02644554976326974e-06, -6.42543741723487294e-06, -6.07103419227907060e-06}, + {3.20307552755319525e-06, -4.83533743093466500e-06, 9.13563764113473065e-07}, + {-2.18105804067510178e-06, 6.19595552598436322e-07, 5.21392855381760945e-06}, + {-2.43310123604345563e-06, 2.17201813434465818e-06, 1.94098874242362718e-07}, + {-1.56293672065252465e-06, 3.95256011818110372e-06, 1.68792962079201969e-06}, + {-1.37567295252127852e-03, 3.59746071987262106e-04, 7.38927139000157259e-05}, + {4.27822004137219658e-06, -8.80187479967658548e-07, 2.29453131891411977e-06}, + {7.68758937964332534e-06, 2.40909410585557829e-07, 4.69351234070854509e-06}, + {-2.87166709944317033e-06, 7.60223902901142716e-07, 4.57864913314467992e-06}, + {-4.01295140267654560e-06, 2.65929275888376483e-06, -2.36575067819565221e-06}, + {2.32693030513910805e-07, 2.28814396769890308e-06, 1.83526107699893970e-07}, + {-2.18213927011287265e-03, 1.65013083920367864e-03, 2.31992998847323087e-04}, + {-7.70829764693697905e-06, 4.23888841240673345e-07, 7.30018322002944087e-06}, + {-1.23111329452911533e-06, 1.50076529718910084e-06, -1.91139744928209288e-06}, + {-1.68872756433485760e-06, 1.03254236824697979e-06, -1.72081108163607555e-06}, + {1.64276928199709460e-06, -4.96350219553231067e-07, -1.46349385185670297e-06}, + {1.12731767057843682e-03, 5.03104281148445223e-04, 1.36398977654308994e-03}, + {-1.05449609518089293e-06, -4.06952115309007489e-07, 3.53062441379482783e-06}, + {-1.98745923822574166e-06, 4.98021943693208180e-07, 3.92645061370218429e-06}, + {-1.55569377977005097e-07, -4.00262856484093037e-07, -2.49609122397048688e-06}, + {2.18005022830924673e-03, -4.10275057064835439e-05, -2.59776311836759947e-04}, + {5.41337439827552225e-07, -1.88603932528607146e-06, -2.06428606152470051e-06}, + {-6.03243799807140491e-06, -3.75067864464502022e-06, -3.05702776851046742e-06}, + {2.30038011634901016e-03, -1.32581161861259635e-03, -1.07680096899188406e-03}, + {4.46773877910556887e-06, 1.85008408528524772e-08, -2.72851357570281713e-06}, + {-1.49177636513049289e-03, -1.91426739654176659e-04, -1.71206384332753194e-03}, + {2.31661325589237743e-02, 2.26540538563063554e-01, 5.42330337046266139e-02}, + {-1.40563059963100256e-06, -4.50551806294901061e-06, 8.87542894832671347e-06}, + {-1.66780916452391459e-06, 4.12065434881171526e-06, -3.55865035776836702e-06}, + {2.71536622051954390e-07, -3.08564858926584692e-06, -1.52164363662402047e-06}, + {2.66659632027280158e-06, -1.19436686895073481e-06, -3.25738306279285683e-06}, + {-1.43666282346327501e-06, -2.51923473623639690e-06, 5.21205120344175876e-06}, + {2.82954522469612199e-04, -1.59147454710008968e-03, 1.27685773978167098e-03}, + {-3.99471240294241303e-06, 9.97323772325767188e-08, -5.28196823261495307e-06}, + {-6.39858432699424995e-06, -4.59897864440506933e-06, -2.39736149785715891e-06}, + {2.89457420106498109e-06, -3.10427512149489757e-06, 9.75553221437691631e-07}, + {-8.96518259720091581e-07, -5.53996694461914366e-06, 1.03733964032237669e-05}, + {8.82130497168875905e-04, -2.33618402105562365e-03, 1.35100410641244379e-03}, + {-2.14088521029685841e-06, 2.59005410360388117e-06, -9.78713171504927426e-08}, + {-4.50668337071552516e-06, 3.58808570076458002e-06, -1.56159349007541082e-06}, + {-1.52345101244247272e-06, 2.21066768791959578e-06, -2.19555898547246775e-06}, + {2.07334042074768356e-03, -1.56333498489329517e-03, -5.53762940364141767e-04}, + {2.22151748134440108e-06, -4.74729938900429749e-07, -3.46744150304684889e-06}, + {2.95389009221172505e-06, -2.96312023445686329e-06, -9.00385068308695580e-07}, + {-6.47780848348620771e-04, 2.38772263398574292e-03, -8.93908589731968019e-04}, + {9.69501567645025819e-07, 2.41432205872957328e-06, 5.56908291093893837e-07}, + {-6.33392066185247586e-04, 2.38613844267241120e-03, -1.05383725637261472e-03}, + {6.76250135616376785e-02, -5.57799579151454852e-02, 1.83393652374666566e-01}, + {3.53986894266120067e-06, 5.92996717102502093e-06, -7.32378536156402804e-06}, + {5.69667193362453916e-06, 1.20219201908705218e-06, -4.56663805956276925e-06}, + {7.11494218295222192e-07, 2.93069858359131137e-06, 1.23210839732268429e-07}, + {-3.41917893741799928e-06, -1.47435291776966751e-06, 1.07397354370819542e-06}, + {7.30931882734254710e-04, 1.15433149094644884e-03, -2.40026982569019722e-03}, + {-1.22780859907432871e-06, 2.29287908084027789e-06, 1.84270754640877832e-06}, + {7.71579140080615178e-07, 2.92378122615943208e-06, -1.91800935486416413e-07}, + {-3.76107279903559188e-07, -1.83159743461489867e-06, 8.17089655984204466e-07}, + {-1.10830882430058061e-03, -5.10908079549339251e-04, -1.77835176235151705e-03}, + {-1.26839781743699406e-06, -2.86942252006448415e-06, 4.47464983859263005e-06}, + {-1.44518716284694482e-06, -7.03360635528004451e-06, 1.04898109513258675e-05}, + {-4.98687888007460470e-04, 1.86990180752567262e-03, -1.24341018156770089e-03}, + {-2.90479801332704790e-06, -9.24272269110706229e-07, 7.56354222045119151e-07}, + {-1.16451534008294149e-03, -2.34216801827852273e-03, 4.91479264672447288e-03}, + {-7.70970926241258958e-02, 9.35855573900774423e-02, 1.50623807158846906e-01}, + {1.14039905307547484e-06, -1.80664235182388840e-07, -5.15527441317074897e-06}, + {7.50559587697416375e-06, -6.23982034686780714e-06, -5.01245198064126721e-06}, + {2.37840954889385892e-06, -4.15663063190341991e-06, 1.93118829429697603e-06}, + {-1.54903048110950777e-03, 2.65832194444263125e-04, 5.34401520444913940e-04}, + {4.00040634507183718e-06, -2.43965474694277443e-06, 2.88683251413283937e-06}, + {7.72301916160400559e-06, -9.54300275625495457e-07, 5.50777546561020959e-06}, + {-2.28103126593574368e-03, 1.02658341009706066e-03, 1.22010567464172614e-03}, + {-6.32818026002207601e-06, 9.83088209200334157e-07, 5.24316808343458507e-06}, + {1.37175660779395581e-03, 4.01188715721313943e-04, 7.59370199245276625e-04}, + {-3.33184694847917573e-01, 7.82846225823195241e-02, -9.94270054263078074e-02}, + {-1.70108770909324636e-06, -5.10749831734438279e-06, 9.80267482880020635e-06}, + {-1.79301365419055891e-06, 4.44839673308561508e-06, -3.83837422072638712e-06}, + {1.71911692904483371e-04, -1.56077480341044431e-03, 1.30725115579017584e-03}, + {-3.55763938679129477e-06, 1.20558966207589408e-06, -5.94340114624253291e-06}, + {1.02325453537648178e-03, -1.52640960762801372e-03, 3.10973117856692537e-04}, + {3.81842873295820109e-03, -3.02114884453467680e-01, 2.78264587142456665e-01}, + {3.46123498726202961e-06, 5.05929187103208375e-06, -6.85764673719752027e-06}, + {4.47228353489932293e-04, 9.60672217798415784e-04, -2.19382758010531077e-03}, + {2.22711833124298791e-01, -4.14141995162802465e-02, -4.27998216564745015e-01}, + {-1.78271151817048783e-03, -9.81039111371464307e-04, -1.37513011841553174e-03}, + {3.35305394560947434e-10, -1.26710751613412498e-09, 3.54248685940916630e-09}, + {-9.26917423371698135e-09, 6.21190912597491263e-09, 1.86942252233812667e-08}, + {-1.56687696151180944e-09, -5.44315731376698864e-09, 1.93822974337010123e-09}, + {7.52897716393974292e-10, -3.48923168136394679e-10, -5.94217786087369859e-10}, + {2.52116855170569920e-10, -2.48216903975251313e-09, 1.01699001303634518e-09}, + {3.72215577457146729e-09, 4.51910314724912610e-10, -6.15361639422218332e-09}, + {-2.62088816666700142e-07, 3.23631086683010168e-07, 8.85302852722882894e-07}, + {-1.30537319842360944e-08, 1.46808588619151692e-08, 2.67574040702101001e-09}, + {-1.23991327621864045e-08, 2.61298349069072344e-08, -4.58919307373337193e-09}, + {5.03079244928983371e-09, -6.73783119575777079e-10, -1.13935871848269699e-08}, + {9.09065785148488459e-09, -1.04304054004966673e-08, -3.23123813816827976e-09}, + {9.55627910137479830e-10, -1.41129563591135820e-08, -1.75594400131373618e-09}, + {-1.05549669436946769e-07, 8.47284096194811896e-08, 6.70761880091491625e-07}, + {-5.92079330008488114e-10, 6.31702118392141188e-09, -4.51534448719925763e-09}, + {-1.04033970327321867e-09, 4.67775485013532943e-09, 2.79348504744758586e-09}, + {5.38758108958869997e-09, -9.55380699552144108e-09, 6.16488249338686956e-11}, + {1.12057409185073453e-09, -3.00645183748393663e-09, -2.14940637510707688e-09}, + {-6.27004681934967278e-07, 8.59159786402940127e-07, 2.73192537668387470e-07}, + {7.36784189214745311e-10, -8.12761968838060511e-10, -2.43226564583531868e-09}, + {1.25546123497244366e-09, -6.98609614602219153e-10, -5.29894812750786315e-09}, + {-8.88351475714088679e-10, 1.37132565025677167e-09, 1.92497813869541012e-09}, + {6.10992637326349119e-07, -6.13496367368217277e-07, -2.19901889726877020e-06}, + {-8.59090437677068053e-11, 2.72772732179404898e-09, 1.54554039011323141e-09}, + {-4.58798915525804318e-10, 4.54384851966693759e-09, 3.63189350816028877e-09}, + {9.93115786933340683e-08, 1.63700862245048928e-07, -1.71397937400244449e-07}, + {-1.62985361318312982e-09, -3.10762126448649312e-09, 1.76193495557419588e-09}, + {6.27207737564569601e-07, -1.49343052365004934e-06, 8.16168870109573730e-08}, + {1.42518738380244172e-03, -3.47531891583186285e-04, -2.98661838800559913e-04}, + {8.98157254125564464e-09, -8.24242643235328920e-09, -5.34769730234363472e-09}, + {-2.17776999489327494e-08, -4.47141107473569832e-09, -1.10218517090920898e-08}, + {3.19614509858290319e-09, -3.32861183754973311e-09, 9.92016746526047655e-11}, + {-2.91660393059167689e-09, 5.59829099744391101e-09, 1.70080685646389895e-09}, + {1.22479524179014421e-09, 9.20737683318684219e-09, -1.10618757209746121e-10}, + {7.70594587548882257e-09, -1.33267446898667659e-06, 4.52812675308736368e-07}, + {9.46080642993951670e-09, -1.95483249032513129e-08, -1.23592694620255905e-08}, + {-2.02330094345448686e-09, 1.18198534293512125e-10, 2.34746776184291406e-09}, + {4.00839940406516604e-09, -4.80716730311137042e-09, 5.25802457129742606e-09}, + {-2.53115202408782380e-09, 2.05563177591017165e-10, 5.46003270374129102e-09}, + {3.24841319972028232e-08, -1.24284705839720552e-06, 4.97326549863015555e-07}, + {1.37729661009444726e-09, -1.67903983772088594e-09, -5.62083748989472554e-09}, + {-3.53256937590806785e-10, 4.49320892992322030e-09, -4.02300486673778934e-09}, + {2.48976475547557641e-09, -6.97256366533061112e-09, 1.43185084622299286e-09}, + {-4.38617299338556199e-09, 9.45081248826811111e-08, -2.91197460585562728e-07}, + {3.24429103026879773e-09, -1.71647943601749287e-09, 2.71076100455402980e-09}, + {3.86933235105302309e-09, -2.82628156988984358e-09, 8.24455756442965537e-09}, + {-7.46614068323353530e-07, 1.27696340529665289e-06, 6.88413034833322557e-07}, + {-5.78118683480788320e-09, 1.34319005917760137e-09, -1.15898873831454807e-09}, + {4.42686972671260670e-07, 6.41810588767341775e-07, -1.16058405342719939e-08}, + {2.24399192788231686e-03, -1.35129336477888174e-03, -7.39944244498236844e-04}, + {7.47869199901884940e-09, -2.68762612165573955e-09, -7.41584788022109365e-09}, + {1.80867308283150230e-09, -2.21500551234043996e-09, 1.86995768869380186e-09}, + {-5.05514829302056157e-09, 4.74048706539109688e-09, 2.52998993977016085e-09}, + {1.32441967115592973e-09, 5.70339246663831290e-09, 7.13448300437846683e-10}, + {1.19767475292940212e-06, 6.72445227582811568e-07, -1.97500319605841551e-06}, + {-1.70612399208458498e-09, 1.07145120553653328e-09, 1.73225882249550267e-09}, + {1.15369127445807962e-09, -5.80362996549510513e-09, 9.33515653667171819e-10}, + {3.38692740520230018e-09, 3.72531013675958533e-09, -3.18062756687886861e-09}, + {1.14787653780236421e-06, -1.84917201319622368e-06, -2.44834286920736499e-07}, + {1.45558928799083276e-09, 1.12720083267348059e-09, 9.00940544390493869e-10}, + {2.09654001104286891e-09, 4.92913422578400429e-09, 3.04938074791039071e-10}, + {3.54033623213741155e-07, 1.07259516691213860e-06, -6.03027205987524684e-07}, + {-2.72038239157446071e-09, -1.60070143945256760e-09, 6.03853855807301443e-10}, + {-2.03235662485238069e-06, -1.03151962834260348e-06, 1.99637918628457062e-06}, + {-1.26261175077493210e-03, -4.98503988506484859e-04, -1.03875859619143593e-03}, + {6.43182729298530376e-10, 8.01776645076301975e-10, -1.83589794755523172e-09}, + {4.01805119037978997e-09, -5.63673552278487477e-10, -1.09102650663883693e-08}, + {-1.48648961195707585e-09, 5.01067861508053269e-09, 2.99132781045319263e-09}, + {-8.91404754824534629e-07, 7.49163968581634775e-07, 2.12542215183124383e-06}, + {2.38642574451608525e-09, -3.47605810802065207e-09, 3.86935566920598717e-10}, + {-2.80031986488182838e-09, -4.25160427697246490e-11, 2.24182921879090280e-09}, + {-1.26991357818351247e-07, -1.45348284568834647e-07, 5.68792533226815389e-07}, + {1.39227229745131353e-09, -1.84849578699353145e-09, 2.24967258190267305e-09}, + {-1.15462500328497586e-06, 1.84347590761761086e-06, 3.64918716654494962e-07}, + {-2.09357112083411985e-03, 1.60820400301404873e-05, 2.27418117008655948e-04}, + {-1.04484803378768198e-08, 4.86043558178828050e-09, 2.00996588123336650e-09}, + {1.44040971927772432e-08, 1.42223015309195233e-09, 1.99778974613318283e-09}, + {-1.62414574166394599e-07, -1.31976785339561840e-06, 4.43918084507000099e-07}, + {3.73061943836905385e-09, 1.00036822436866402e-08, -1.05450977117005351e-09}, + {-2.06551932971539565e-07, -9.72167971235462190e-07, 4.28861904300768815e-07}, + {-2.16051814014425313e-03, 1.48780488507118812e-03, 7.79940397419977911e-04}, + {-4.80544204428667854e-09, -1.09870773590259319e-09, 6.58876991984844174e-09}, + {1.31575045692056136e-06, 4.32430764481131318e-07, -1.55255090541518703e-06}, + {1.28823975640215602e-03, 4.04521283440268135e-04, 1.76186984141882253e-03}, + {-1.09767251093991436e-01, -4.94112205838347640e-02, -5.43102978164306804e-02}, + {7.93691223854864347e-10, 1.54639511196208446e-08, -1.71518303448969789e-08}, + {2.56523843833456056e-09, -2.31047392329486456e-09, -4.29758133398648601e-09}, + {-9.87725901069325118e-09, 4.28127375218245732e-09, 2.02888056355376989e-09}, + {3.21762172461603768e-10, -5.82937505211322815e-09, 3.88293127512318037e-09}, + {1.63250610252241302e-09, -7.02161705168347083e-09, 3.46592492032893329e-09}, + {-1.44272117683086343e-07, -4.40408510988914148e-07, 5.92746408872857344e-07}, + {2.71961467235293242e-09, -1.47466668633244868e-08, 2.89637452632884873e-08}, + {1.47637712476396399e-08, 1.16406781783262581e-09, 2.04904540557215853e-09}, + {-5.53709807865621073e-09, 7.05512286092169205e-09, 1.56159114805820565e-09}, + {5.29268649740455288e-09, 2.10616986628942016e-08, -3.03219004488264332e-08}, + {1.79978890693655025e-07, 7.95085399132693105e-07, -4.78366567607801940e-07}, + {-4.03847393894152251e-10, 2.90357085597214848e-09, 1.12992165623992946e-09}, + {2.99031871486832301e-09, -1.37951879780606745e-09, 2.41048263988075107e-09}, + {1.26882357398550027e-09, 1.30631467101793852e-09, 7.99574240151201820e-10}, + {-1.41169562567489137e-08, 1.27148955713198356e-06, -2.89386439707162157e-07}, + {-2.68794415198003733e-09, 8.73673404455654889e-10, 2.89557382238125882e-09}, + {-4.90264437380538709e-09, 1.89207244316591527e-09, 2.25393465003165261e-09}, + {-3.58274654665979853e-08, 2.91386646529383231e-07, -4.98477764412919022e-08}, + {1.65722165851311942e-09, -1.11673743863338615e-09, -4.14131162695952071e-09}, + {-1.47751280626939874e-07, -2.41471865000848773e-07, -8.53552350049691100e-07}, + {-2.24352957583577790e-04, 1.60900273524284708e-03, -1.32260753549593617e-03}, + {2.05497643901431104e-09, 1.38702982710459111e-08, -3.09887516689033582e-09}, + {3.39770491949997755e-09, 9.41613393506957053e-09, -7.09844738544518350e-10}, + {7.86209687630989862e-10, 1.93556837224662104e-10, -6.58630930350234678e-09}, + {-6.86841181152253455e-10, -5.57194149153339424e-09, 1.41214109156129197e-09}, + {2.59516074158083754e-07, 1.30703181255419770e-06, -4.02454784192984860e-07}, + {-5.79425202262839889e-10, 4.05071760856134944e-09, 3.02384985106929349e-09}, + {4.00677924866643664e-09, -2.25614611715219127e-09, 7.52819043214891792e-09}, + {2.34003759425061020e-09, 5.27462258592681366e-09, -2.05723854618256041e-10}, + {2.29340174767722615e-07, 1.05507868574435809e-06, -4.45904844964539748e-07}, + {-3.91634245866523401e-09, 1.07849931763048801e-09, 1.85542686770290288e-09}, + {-6.62166513287765213e-09, 3.86355018811013196e-09, -1.87861701195224384e-09}, + {1.32112240848469842e-07, 4.39339645861430705e-08, -1.59384598983486336e-06}, + {2.02488462108796341e-09, -1.48427112267590644e-09, -4.32055485832805175e-09}, + {-4.27701540045566375e-07, -1.46229443391283215e-06, -2.38186369433401879e-07}, + {-9.86744509368740232e-04, 1.91104095070606826e-03, -8.17774843405986713e-04}, + {2.06891823117949514e-10, -2.64060942556376688e-09, 1.86419366055012858e-09}, + {8.33785634979378187e-09, -1.00697171434571686e-08, -2.84106664583116952e-09}, + {5.07057938692323518e-09, -9.56246298811080919e-09, -6.33399999117045809e-11}, + {-6.78808357162941078e-08, -2.21612941845184680e-07, 9.42031624998063144e-08}, + {-3.04300065007145903e-09, 5.64120231083542478e-09, 1.65718606892628628e-09}, + {3.76240642807612602e-09, -4.58941407446844529e-09, 5.06162500801821125e-09}, + {7.25149885354159363e-07, -1.18149759075966698e-06, -6.82406347277120240e-07}, + {-4.84358128605144600e-09, 4.56893046833772853e-09, 2.67044331092591847e-09}, + {-2.54939737986958903e-07, -1.06106228658746360e-06, 5.04013386790069795e-07}, + {-2.17097468872509735e-03, 1.41624400187313607e-03, 8.11305605779899562e-04}, + {2.24635331169675823e-10, -6.02144184513875302e-09, 4.15827878380570226e-09}, + {-4.55408258326350790e-09, 6.20319154376325343e-09, 2.08760821823750220e-09}, + {2.10871853867367065e-07, -4.29346688506603014e-07, 1.15683623843482186e-07}, + {1.00732072683129559e-09, 3.88267751283422058e-11, -6.73798626615873530e-09}, + {5.34506627847264326e-09, -8.01262819982717645e-08, 1.60888846226225901e-06}, + {5.83419066552946048e-04, -2.36474094848551555e-03, 8.79373865688287898e-04}, + {-4.85158746510450101e-10, -6.78789624508624456e-09, 4.95385649168511577e-09}, + {3.47485142271342085e-07, 5.60944792101468470e-07, -4.35887910682497548e-07}, + {5.75824910919892421e-04, -2.18618554413632388e-03, 1.22736498224538170e-03}, + {-2.51838883195707221e-02, -8.23487774284355212e-02, 3.33658831723806573e-02}, + {-8.70167529698484543e-09, -1.37080219501928280e-08, 1.80728228771354082e-08}, + {-4.67111571644807100e-09, -2.72041008123058425e-09, 7.06648883852523113e-09}, + {7.26183221906172727e-10, -6.77816339167414128e-09, 4.52883232651690726e-09}, + {5.28852302228433047e-09, 6.47161005340457507e-09, -8.67298467766008940e-09}, + {-2.25465519365641853e-07, -6.46057585221293529e-07, 3.48151143400587948e-07}, + {-1.30051025504229756e-09, -3.25062288891730944e-09, 2.01775679498084060e-09}, + {-5.12724809831333062e-09, 9.33902577666956280e-10, -6.96327353416625883e-10}, + {-3.10810940873373909e-09, -7.49756534634826721e-10, 6.87357185058523612e-10}, + {-1.52109221995821997e-06, -4.22908767925417317e-07, 1.38629667568307413e-06}, + {1.42955317028459206e-09, -7.02968461219199980e-10, -3.81617160094549490e-09}, + {2.53707400921232562e-09, -1.60727622877665510e-09, -4.18765366827500429e-09}, + {-2.14750738948554787e-07, -6.40554276953864132e-07, 3.76128531993924486e-07}, + {3.83073214815787821e-09, 4.50296289838947317e-10, 2.29523194894554194e-09}, + {4.76340728555735282e-07, 6.83235613037347367e-07, -4.72205395646296822e-07}, + {-6.10651996176347607e-04, -1.06790499934057291e-03, 2.29083496655867842e-03}, + {3.95497823379997726e-09, 1.38236928154400474e-09, -6.26218820548585242e-09}, + {1.11904936705986557e-09, -1.37869946362223494e-08, -9.34049783699042457e-10}, + {1.25499246411697740e-09, -2.73635453185150368e-09, -2.91506864740637139e-09}, + {-3.59882924006599270e-07, 1.32511373732895413e-06, -1.55110207063907657e-07}, + {1.07068498511608823e-09, 8.92087770321126072e-09, 2.62826524433101838e-10}, + {-2.69316546841480431e-09, 9.61138280075601870e-10, 5.19946977139973399e-09}, + {-5.92563579700916554e-07, -1.05071339539294234e-06, 1.56249964602256375e-07}, + {1.32198180180509439e-09, 5.16087961255351502e-09, 8.46339526239248130e-10}, + {2.07323220008381881e-06, 1.02309267446332522e-06, -2.07661522726165781e-06}, + {1.31402366846389393e-03, 3.78229792813366064e-04, 1.77496793932758741e-03}, + {8.59301428624004160e-10, -6.83071707530125138e-09, 3.36249680876754553e-09}, + {5.27310424491833629e-09, 2.09999085065692981e-08, -3.10459945807028959e-08}, + {-8.88666080375855039e-08, 4.60897593930476024e-07, 7.41576575386676540e-07}, + {-4.85540663230921155e-10, -5.58243438975036810e-09, 7.40450811775872353e-10}, + {4.03141117225058743e-07, 1.52035531639227450e-06, 9.06206514897367477e-08}, + {5.61075629915620496e-04, -2.05847905628765053e-03, 1.12849817492909434e-03}, + {5.11216541321246609e-09, 7.26292920250060092e-09, -8.97145741030058730e-09}, + {-4.26211688914213127e-07, -7.03366608210270750e-07, 6.27995585866791828e-07}, + {1.15309052943982646e-03, 2.34474318844151959e-03, -4.91856748507475423e-03}, + {1.01104427799588961e-01, -4.22361682938472982e-02, -1.88750007538552200e-01}, + {3.94738332298860684e-10, -7.81372397340440727e-10, 4.06815717224340290e-09}, + {-8.61483928638051566e-09, 5.37427180535843263e-09, 1.81738104426676372e-08}, + {-8.48011268844706123e-10, -5.33803143354383280e-09, 2.99703953494934172e-10}, + {3.89154099408092063e-07, -2.44166311268514957e-07, -8.03240371135063858e-07}, + {-1.20249536439409610e-08, 1.48908931921210019e-08, 1.88292573199966284e-09}, + {-1.16401289163015065e-08, 2.57866422936903206e-08, -5.27022399332555125e-09}, + {1.37065399911928676e-07, 2.16494406102361175e-08, -7.63924557662179482e-07}, + {-6.94754161319199870e-10, 6.65038621394664631e-09, -4.31779645371221932e-09}, + {4.72542155592614588e-07, -7.58546986886782931e-07, -2.35913417925837088e-07}, + {1.46133817312113241e-03, -3.25193103208258009e-04, -3.06625181254991741e-04}, + {9.35794082672593210e-09, -7.92923574022275091e-09, -5.41426242728348939e-09}, + {-2.15279239157428748e-08, -4.16754339024882903e-09, -1.12896482995505920e-08}, + {2.60645369870582400e-10, 1.44616071127263122e-06, -3.63334053799999057e-07}, + {9.17105741349288905e-09, -2.02295233654725681e-08, -1.20002956877085509e-08}, + {-1.27759226226098477e-07, 1.28193771791124470e-06, -5.83097827522305323e-07}, + {2.26880791869919426e-03, -1.34042850080092401e-03, -7.65092051285704835e-04}, + {7.03374036792325796e-09, -2.53508958270032281e-09, -7.66132998708535240e-09}, + {-9.71978722189015265e-07, -5.57836512454779054e-07, 1.96329328074063003e-06}, + {-1.26115140811304343e-03, -4.81792074617704632e-04, -1.06803272537897391e-03}, + {1.19419564863885497e-01, 5.07766738901840875e-02, 4.87642090320925953e-02}, + {1.14090414893297520e-09, 1.56073433760228752e-08, -1.78054684078429726e-08}, + {3.03285130343056153e-09, -1.58615337531031741e-09, -4.94928394101368241e-09}, + {2.64483280249840080e-07, 2.97155396291660413e-07, -5.41608085095034164e-07}, + {2.68757552324139226e-09, -1.41400907649469332e-08, 2.93255796729452456e-08}, + {-2.11094617584561828e-07, -6.56355695552793272e-07, 3.72180321686621518e-07}, + {-2.55073452371079590e-04, 1.57943859317488818e-03, -1.29154484940938240e-03}, + {1.40049266628139435e-09, 1.40747080656922208e-08, -2.58792021839981956e-09}, + {-2.12330362681090179e-07, -1.30522733223815968e-06, 5.84417623253341567e-07}, + {-9.33144849909676392e-04, 1.90305575962152547e-03, -8.35564417983726418e-04}, + {1.81624805201406961e-02, 6.84911174969819458e-02, -2.28291882522520390e-02}, + {-8.25231299961259879e-09, -1.40227519596081152e-08, 1.78809529925716415e-08}, + {1.90689491530449118e-07, 7.01057736002264065e-07, -4.26430629252294580e-07}, + {-5.85146839837499930e-04, -1.07311215649546045e-03, 2.31986890222730339e-03}, + {-1.05962397073886522e-01, 5.51532131360410807e-02, 1.87542648909451215e-01}, + {-1.37499370823599516e-03, -8.49619409242363438e-04, -1.18180356709159952e-03} +}; + +static const double INTERCEPT[3] = { + -1.29208772400146188e+00, + 6.62251952866635918e+00, + -1.35908984683965173e-01 +}; +// END AUTO-GENERATED COEFFICIENTS + +inline void compute_poly_features(const double x[7], double out[330]) { + for (int i = 0; i < N_FEATURES; ++i) { + double val = 1.0; + for (int j = 0; j < N_INPUTS; ++j) { + if (POWERS[i][j] != 0) { + double base = x[j]; + int exp = POWERS[i][j]; + // Fast integer exponentiation (max exp = 4) + double p = 1.0; + for (int e = 0; e < exp; ++e) + p *= base; + val *= p; + } + } + out[i] = val; + } +} + +} // namespace detail + +struct RGB { + unsigned char r, g, b; +}; + +/** + * Mix two RGB colors using polynomial pigment mixing. + * + * This performs polynomial pigment-style RGB interpolation. + * + * @param r1,g1,b1 First color (0-255) + * @param r2,g2,b2 Second color (0-255) + * @param t Mixing ratio: 0.0 = all color1, 1.0 = all color2 + * @param out_r,out_g,out_b Output color (0-255) + */ +inline void lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t, + unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) { + // Clamp t + if (t <= 0.0f) { + *out_r = r1; *out_g = g1; *out_b = b1; + return; + } + if (t >= 1.0f) { + *out_r = r2; *out_g = g2; *out_b = b2; + return; + } + + double x[7] = { + static_cast(r1), static_cast(g1), static_cast(b1), + static_cast(r2), static_cast(g2), static_cast(b2), + static_cast(t) + }; + + double features[330]; + detail::compute_poly_features(x, features); + + // Dot product: features @ COEF + INTERCEPT + for (int c = 0; c < 3; ++c) { + double sum = detail::INTERCEPT[c]; + for (int i = 0; i < detail::N_FEATURES; ++i) { + sum += features[i] * detail::COEF[i][c]; + } + // Clamp to [0, 255] and truncate (matches numpy astype(int) behavior) + int val = static_cast(sum); + if (val < 0) val = 0; + if (val > 255) val = 255; + + if (c == 0) *out_r = static_cast(val); + else if (c == 1) *out_g = static_cast(val); + else *out_b = static_cast(val); + } +} + +/** + * Convenience overload returning an RGB struct. + */ +inline RGB lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t) { + RGB result; + lerp(r1, g1, b1, r2, g2, b2, t, &result.r, &result.g, &result.b); + return result; +} + +} // namespace filament_mixer + +#endif // FILAMENT_MIXER_H diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index d06afca40b..101aa23491 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -178,6 +178,8 @@ set(SLIC3R_GUI_SOURCES GUI/GLCanvas3D.hpp GUI/GLModel.cpp GUI/GLModel.hpp + GUI/MMUPaintedTexturePreview.cpp + GUI/MMUPaintedTexturePreview.hpp GUI/GLSelectionRectangle.cpp GUI/GLSelectionRectangle.hpp GUI/GLShader.cpp diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 89110b0432..f0ef99c381 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -2,6 +2,7 @@ #include "3DScene.hpp" #include "GLShader.hpp" +#include "MMUPaintedTexturePreview.hpp" #include "GUI_App.hpp" #include "GUI_Colors.hpp" #include "Plater.hpp" @@ -22,11 +23,13 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Tesselate.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/TextureMapping.hpp" #include #include #include #include +#include #include @@ -85,6 +88,42 @@ Slic3r::ColorRGBA adjust_color_for_rendering(const Slic3r::ColorRGBA &colors) namespace Slic3r { +namespace { + +std::vector build_full_mesh_texture_preview_triangles(const ModelVolume &model_volume) +{ + std::vector out; + const indexed_triangle_set &its = model_volume.mesh().its; + out.reserve(its.indices.size()); + for (size_t triangle_idx = 0; triangle_idx < its.indices.size(); ++triangle_idx) { + const stl_triangle_vertex_indices &triangle = its.indices[triangle_idx]; + if (triangle[0] < 0 || triangle[1] < 0 || triangle[2] < 0) + continue; + if (size_t(triangle[0]) >= its.vertices.size() || + size_t(triangle[1]) >= its.vertices.size() || + size_t(triangle[2]) >= its.vertices.size()) + continue; + + TriangleSelector::FacetStateTriangle facet; + facet.source_triangle = int(triangle_idx); + facet.vertices[0] = its.vertices[size_t(triangle[0])].cast(); + facet.vertices[1] = its.vertices[size_t(triangle[1])].cast(); + facet.vertices[2] = its.vertices[size_t(triangle[2])].cast(); + out.emplace_back(std::move(facet)); + } + return out; +} + +bool model_volume_has_any_texture_preview_data(const ModelVolume &model_volume) +{ + return !model_volume.imported_vertex_colors_rgba.empty() || + (!model_volume.imported_texture_rgba.empty() && + model_volume.imported_texture_width > 0 && + model_volume.imported_texture_height > 0); +} + +} // namespace + const float GLVolume::SinkingContours::HalfWidth = 0.25f; @@ -582,20 +621,85 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj if (volume_idx() >= model_object->volumes.size()) break; model_volume = model_object->volumes[volume_idx()]; - if (model_volume->mmu_segmentation_facets.empty()) + const size_t num_physical = std::max(0, GUI::wxGetApp().filaments_cnt()); + const TextureMappingManager *texture_mgr = GUI::wxGetApp().preset_bundle != nullptr ? + &GUI::wxGetApp().preset_bundle->texture_mapping_zones : nullptr; + const unsigned int base_filament_id = model_volume->extruder_id() > 0 ? unsigned(model_volume->extruder_id()) : 0u; + const TextureMappingZone *base_zone = texture_mgr != nullptr ? texture_mgr->zone_from_id(base_filament_id) : nullptr; + const bool has_mmu_segmentation = !model_volume->mmu_segmentation_facets.empty(); + const bool has_texture_preview = + base_zone != nullptr && + base_zone->enabled && + !base_zone->deleted && + (base_zone->is_2d_gradient() || (base_zone->is_image_texture() && model_volume_has_any_texture_preview_data(*model_volume))); + if (!has_mmu_segmentation && !has_texture_preview) + { + mmuseg_texture_preview.reset(); + mmuseg_texture_preview_signature = 0; break; + } color_volume = true; - if (model_volume->mmu_segmentation_facets.timestamp() != mmuseg_ts) { + size_t preview_visual_signature = texture_preview_settings_signature(num_physical, texture_mgr); + preview_visual_signature ^= model_volume_texture_preview_signature(*model_volume) + 0x9e3779b97f4a7c15ull + + (preview_visual_signature << 6) + (preview_visual_signature >> 2); + preview_visual_signature ^= model_volume->imported_vertex_colors_rgba.size() + 0x9e3779b97f4a7c15ull + + (preview_visual_signature << 6) + (preview_visual_signature >> 2); + preview_visual_signature ^= reinterpret_cast(model_volume->imported_vertex_colors_rgba.data()) + 0x9e3779b97f4a7c15ull + + (preview_visual_signature << 6) + (preview_visual_signature >> 2); + if (model_volume->mmu_segmentation_facets.timestamp() != mmuseg_ts || + preview_visual_signature != mmuseg_texture_preview_visual_signature) { mmuseg_models.clear(); - std::vector its_per_color; - model_volume->mmu_segmentation_facets.get_facets(*model_volume, its_per_color); - mmuseg_models.resize(its_per_color.size()); - for (int idx = 0; idx < its_per_color.size(); idx++) { - mmuseg_models[idx].init_from(its_per_color[idx]); + mmuseg_texture_preview_models.clear(); + mmuseg_texture_preview_colors.clear(); + mmuseg_texture_preview_filament_ids.clear(); + mmuseg_vertex_color_preview_models.clear(); + mmuseg_vertex_color_preview_colors.clear(); + mmuseg_vertex_color_preview_filament_ids.clear(); + + std::vector> triangles_per_type; + if (has_mmu_segmentation) { + std::vector its_per_color; + model_volume->mmu_segmentation_facets.get_facets(*model_volume, its_per_color); + mmuseg_models.resize(its_per_color.size()); + for (int idx = 0; idx < its_per_color.size(); idx++) { + mmuseg_models[idx].init_from(its_per_color[idx]); + } + model_volume->mmu_segmentation_facets.get_facet_triangles(*model_volume, triangles_per_type); + } else { + triangles_per_type.resize(1); + triangles_per_type[0] = build_full_mesh_texture_preview_triangles(*model_volume); } + std::vector state_colors; + const int extruder_id = model_volume->extruder_id(); + const ColorRGBA fallback_color = extruder_colors.empty() ? ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : extruder_colors.front(); + state_colors.emplace_back(extruder_id > 0 && size_t(extruder_id - 1) < extruder_colors.size() ? + extruder_colors[size_t(extruder_id - 1)] : + fallback_color); + state_colors.insert(state_colors.end(), extruder_colors.begin(), extruder_colors.end()); + + build_mmu_texture_preview_models(*model_volume, + triangles_per_type, + state_colors, + base_filament_id, + num_physical, + texture_mgr, + mmuseg_texture_preview_models, + mmuseg_texture_preview_colors, + mmuseg_texture_preview_filament_ids); + build_mmu_vertex_color_preview_models(*model_volume, + triangles_per_type, + state_colors, + base_filament_id, + num_physical, + texture_mgr, + this->world_matrix(), + mmuseg_vertex_color_preview_models, + mmuseg_vertex_color_preview_colors, + mmuseg_vertex_color_preview_filament_ids); mmuseg_ts = model_volume->mmu_segmentation_facets.timestamp(); + mmuseg_texture_preview_visual_signature = preview_visual_signature; } } while (0); @@ -606,47 +710,96 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj extruder_color.a(render_color.a()); } - for (int idx = 0; idx < mmuseg_models.size(); idx++) { - GUI::GLModel &m = mmuseg_models[idx]; - if (!m.is_initialized()) - continue; + if (mmuseg_models.empty()) { + if (tverts_range == std::make_pair(0, -1)) + model.render(shader); + else + model.render(this->tverts_range, shader); + } else { + for (int idx = 0; idx < mmuseg_models.size(); idx++) { + GUI::GLModel &m = mmuseg_models[idx]; + if (!m.is_initialized()) + continue; - if (shader) { - if (idx == 0) { - int extruder_id = model_volume->extruder_id(); - //to make black not too hard too see - ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]); - if (ban_light) { - new_color[3] = (255 - (extruder_id - 1))/255.0f; - } - m.set_color(new_color); - // shader->set_uniform("uniform_color", new_color); - } - else { - if (idx <= extruder_colors.size()) { - //to make black not too hard too see - ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]); + if (shader) { + if (idx == 0) { + int extruder_id = model_volume->extruder_id(); + ColorRGBA new_color = extruder_id > 0 && size_t(extruder_id - 1) < extruder_colors.size() ? + adjust_color_for_rendering(extruder_colors[size_t(extruder_id - 1)]) : + (extruder_colors.empty() ? ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : adjust_color_for_rendering(extruder_colors.front())); if (ban_light) { - new_color[3] = (255 - (idx - 1))/255.0f; + new_color[3] = (255 - std::max(0, extruder_id - 1))/255.0f; } m.set_color(new_color); - // shader->set_uniform("uniform_color", new_color); } else { - //to make black not too hard too see - ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[0]); - if (ban_light) { - new_color[3] = (255 - 0) / 255.0f; + if (idx <= extruder_colors.size()) { + ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]); + if (ban_light) { + new_color[3] = (255 - (idx - 1))/255.0f; + } + m.set_color(new_color); + } + else { + ColorRGBA new_color = extruder_colors.empty() ? ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : adjust_color_for_rendering(extruder_colors[0]); + if (ban_light) { + new_color[3] = (255 - 0) / 255.0f; + } + m.set_color(new_color); } - m.set_color(new_color); - // shader->set_uniform("uniform_color", new_color); } } + if (tverts_range == std::make_pair(0, -1)) + m.render(shader); + else + m.render(this->tverts_range, shader); } - if (tverts_range == std::make_pair(0, -1)) - m.render(shader); - else - m.render(this->tverts_range, shader); + } + + if (!mmuseg_texture_preview_models.empty() || !mmuseg_vertex_color_preview_models.empty()) { + auto adjusted_preview_colors = [](const std::vector &colors) { + std::vector preview_colors = colors; + for (ColorRGBA &preview_color : preview_colors) + preview_color = adjust_color_for_rendering(preview_color); + return preview_colors; + }; + const size_t num_physical = std::max(0, GUI::wxGetApp().filaments_cnt()); + const TextureMappingManager *texture_mgr = GUI::wxGetApp().preset_bundle != nullptr ? + &GUI::wxGetApp().preset_bundle->texture_mapping_zones : nullptr; + const Transform3d model_matrix = this->world_matrix(); + const GUI::Camera& camera = GUI::wxGetApp().plater()->get_camera(); + const std::array z_range = { -std::numeric_limits::max(), std::numeric_limits::max() }; + const std::array clipping_plane = { 0.f, 0.f, 1.f, std::numeric_limits::max() }; + + if (!mmuseg_texture_preview_models.empty() && + ensure_model_volume_texture_preview(*model_volume, mmuseg_texture_preview, mmuseg_texture_preview_signature)) { + render_model_texture_preview_models(mmuseg_texture_preview_models, + adjusted_preview_colors(mmuseg_texture_preview_colors), + mmuseg_texture_preview_filament_ids, + num_physical, + texture_mgr, + *model_volume, + mmuseg_texture_preview, + model_matrix, + camera.get_view_matrix(), + camera.get_projection_matrix(), + z_range, + clipping_plane); + } + if (!mmuseg_vertex_color_preview_models.empty()) { + render_model_vertex_color_preview_models(mmuseg_vertex_color_preview_models, + adjusted_preview_colors(mmuseg_vertex_color_preview_colors), + mmuseg_vertex_color_preview_filament_ids, + num_physical, + texture_mgr, + model_matrix, + camera.get_view_matrix(), + camera.get_projection_matrix(), + z_range, + clipping_plane); + } + if (shader != nullptr) + shader->start_using(); } } else { if (tverts_range == std::make_pair(0, -1)) diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index b12d048aa9..c384b8002f 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -13,6 +13,7 @@ #include "libslic3r/ObjectID.hpp" #include "GLModel.hpp" +#include "GLTexture.hpp" #include "GLShader.hpp" #include "MeshUtils.hpp" @@ -231,6 +232,15 @@ public: // BBS mutable std::vector mmuseg_models; mutable ObjectBase::Timestamp mmuseg_ts; + mutable std::vector mmuseg_texture_preview_models; + mutable std::vector mmuseg_texture_preview_colors; + mutable std::vector mmuseg_texture_preview_filament_ids; + mutable std::vector mmuseg_vertex_color_preview_models; + mutable std::vector mmuseg_vertex_color_preview_colors; + mutable std::vector mmuseg_vertex_color_preview_filament_ids; + mutable GUI::GLTexture mmuseg_texture_preview; + mutable size_t mmuseg_texture_preview_signature { 0 }; + mutable size_t mmuseg_texture_preview_visual_signature { 0 }; // Ranges of triangle and quad indices to be rendered. std::pair tverts_range; @@ -391,6 +401,8 @@ typedef std::vector GLVolumePtrs; typedef std::pair> GLVolumeWithIdAndZ; typedef std::vector GLVolumeWithIdAndZList; +void clear_texture_preview_simulation_cache(); + class GLVolumeCollection { public: @@ -501,7 +513,7 @@ public: ) const; // Clear the geometry - void clear() { for (auto *v : volumes) delete v; volumes.clear(); } + void clear() { clear_texture_preview_simulation_cache(); for (auto *v : volumes) delete v; volumes.clear(); } bool empty() const { return volumes.empty(); } void set_range(double low, double high) { for (GLVolume *vol : this->volumes) vol->set_range(low, high); } diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp index 4c16ce8deb..15ee1872af 100644 --- a/src/slic3r/GUI/ExtraRenderers.cpp +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -313,16 +313,34 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR 0, nullptr, wxCB_READONLY | CB_NO_DROP_ICON | CB_NO_TEXT); c_editor->GetDropDown().SetUseContentWidth(true); - if (has_default_extruder && has_default_extruder()) + int selection_to_set = wxNOT_FOUND; + if (has_default_extruder && has_default_extruder()) { c_editor->Append(_L("default"), *get_default_extruder_color_icon()); + selection_to_set = 0; + } - for (size_t i = 0; i < icons.size(); i++) - c_editor->Append(wxString::Format("%d", i+1), *icons[i]); + std::vector ordered_filament_ids; + if (Slic3r::GUI::wxGetApp().plater() != nullptr) + ordered_filament_ids = Slic3r::GUI::wxGetApp().plater()->sidebar().get_ui_ordered_filament_ids(); + if (ordered_filament_ids.empty()) { + ordered_filament_ids.reserve(icons.size()); + for (size_t i = 0; i < icons.size(); ++i) + ordered_filament_ids.emplace_back(unsigned(i + 1)); + } - if (has_default_extruder && has_default_extruder()) - c_editor->SetSelection(atoi(data.GetText().c_str())); - else - c_editor->SetSelection(atoi(data.GetText().c_str()) - 1); + const int current_extruder = atoi(data.GetText().c_str()); + for (const unsigned int filament_id : ordered_filament_ids) { + if (filament_id == 0 || filament_id > icons.size()) + continue; + const int item_idx = c_editor->GetCount(); + c_editor->Append(wxString::Format("%u", filament_id), *icons[size_t(filament_id - 1)]); + if (current_extruder == int(filament_id)) + selection_to_set = item_idx; + } + + if (selection_to_set == wxNOT_FOUND) + selection_to_set = 0; + c_editor->SetSelection(selection_to_set); // Open the dropdown immediately when the editor is focused. c_editor->Bind(wxEVT_SET_FOCUS, [c_editor](wxFocusEvent& evt) { @@ -389,4 +407,3 @@ wxSize TextRenderer::GetSize() const return GetTextExtent(m_value); } - diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 642ec7f36f..0060ff3512 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -27,6 +27,7 @@ #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Colors.hpp" +#include "MMUPaintedTexturePreview.hpp" #include "Mouse3DController.hpp" #include "I18N.hpp" #include "NotificationManager.hpp" @@ -3166,6 +3167,8 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt) m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this); auto gizmo = wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().get_current(); if (gizmo != nullptr) m_dirty |= gizmo->update_items_state(); + const bool texture_preview_generation_pending = Slic3r::texture_preview_simulation_is_pending(); + m_dirty |= texture_preview_generation_pending; #if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT // ImGuiWrapper::m_requires_extra_frame may have been set by a render made outside of the OnIdle mechanism bool imgui_requires_extra_frame = wxGetApp().imgui()->requires_extra_frame(); @@ -3185,9 +3188,10 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt) _refresh_if_shown_on_screen(); #if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT - if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || wxGetApp().imgui()->requires_extra_frame()) { + if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || + wxGetApp().imgui()->requires_extra_frame() || texture_preview_generation_pending) { #else - if (m_extra_frame_requested || mouse3d_controller_applied) { + if (m_extra_frame_requested || mouse3d_controller_applied || texture_preview_generation_pending) { m_dirty = true; #endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT m_extra_frame_requested = false; @@ -7856,6 +7860,29 @@ void GLCanvas3D::_render_overlays() } m_labels.render(sorted_instances); + if (Slic3r::texture_preview_simulation_is_pending()) { + ImGuiWrapper &imgui = *wxGetApp().imgui(); + const Size cnv_size = get_canvas_size(); + const float scale = imgui.get_style_scaling(); + const float margin = 16.0f * scale; + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * scale); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 8.0f) * scale); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.09f, 0.10f, 0.88f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + imgui.set_next_window_pos(margin, float(cnv_size.get_height()) - margin, ImGuiCond_Always, 0.0f, 1.0f); + imgui.begin(wxString("texture_preview_generation_status"), + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoMouseInputs | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoFocusOnAppearing); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + imgui.text(_L("Generating simulated color preview...")); + imgui.end(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(2); + } + _render_3d_navigator(); _render_canvas_toolbar(); diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 97a34dca85..db84adda9d 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -110,6 +110,21 @@ void GLModel::Geometry::add_vertex(const Vec3f& position, const Vec3f& normal, c vertices.emplace_back(tex_coord.y()); } +void GLModel::Geometry::add_vertex(const Vec3f& position, const Vec3f& normal, const ColorRGBA& color) +{ + assert(format.vertex_layout == EVertexLayout::P3N3C4); + vertices.emplace_back(position.x()); + vertices.emplace_back(position.y()); + vertices.emplace_back(position.z()); + vertices.emplace_back(normal.x()); + vertices.emplace_back(normal.y()); + vertices.emplace_back(normal.z()); + vertices.emplace_back(color.r()); + vertices.emplace_back(color.g()); + vertices.emplace_back(color.b()); + vertices.emplace_back(color.a()); +} + void GLModel::Geometry::add_vertex(const Vec4f& position) { assert(format.vertex_layout == EVertexLayout::P4); @@ -205,6 +220,23 @@ Vec2f GLModel::Geometry::extract_tex_coord_2(size_t id) const return { *(start + 0), *(start + 1) }; } +ColorRGBA GLModel::Geometry::extract_color_4(size_t id) const +{ + const size_t c_stride = color_stride_floats(format); + if (c_stride != 4) { + assert(false); + return ColorRGBA::BLACK(); + } + + if (vertices_count() <= id) { + assert(false); + return ColorRGBA::BLACK(); + } + + const float* start = &vertices[id * vertex_stride_floats(format) + color_offset_floats(format)]; + return { *(start + 0), *(start + 1), *(start + 2), *(start + 3) }; +} + void GLModel::Geometry::set_vertex(size_t id, const Vec3f& position, const Vec3f& normal) { assert(format.vertex_layout == EVertexLayout::P3N3); @@ -272,6 +304,7 @@ size_t GLModel::Geometry::vertex_stride_floats(const Format& format) case EVertexLayout::P3T2: { return 5; } case EVertexLayout::P3N3: { return 6; } case EVertexLayout::P3N3T2: { return 8; } + case EVertexLayout::P3N3C4: { return 10; } case EVertexLayout::P4: { return 4; } default: { assert(false); return 0; } }; @@ -287,6 +320,7 @@ size_t GLModel::Geometry::position_stride_floats(const Format& format) case EVertexLayout::P3T2: case EVertexLayout::P3N3: case EVertexLayout::P3N3T2: { return 3; } + case EVertexLayout::P3N3C4: { return 3; } case EVertexLayout::P4: { return 4; } default: { assert(false); return 0; } }; @@ -302,6 +336,7 @@ size_t GLModel::Geometry::position_offset_floats(const Format& format) case EVertexLayout::P3T2: case EVertexLayout::P3N3: case EVertexLayout::P3N3T2: + case EVertexLayout::P3N3C4: case EVertexLayout::P4: { return 0; } default: { assert(false); return 0; } }; @@ -312,7 +347,8 @@ size_t GLModel::Geometry::normal_stride_floats(const Format& format) switch (format.vertex_layout) { case EVertexLayout::P3N3: - case EVertexLayout::P3N3T2: { return 3; } + case EVertexLayout::P3N3T2: + case EVertexLayout::P3N3C4: { return 3; } default: { assert(false); return 0; } }; } @@ -322,7 +358,8 @@ size_t GLModel::Geometry::normal_offset_floats(const Format& format) switch (format.vertex_layout) { case EVertexLayout::P3N3: - case EVertexLayout::P3N3T2: { return 3; } + case EVertexLayout::P3N3T2: + case EVertexLayout::P3N3C4: { return 3; } default: { assert(false); return 0; } }; } @@ -349,6 +386,24 @@ size_t GLModel::Geometry::tex_coord_offset_floats(const Format& format) }; } +size_t GLModel::Geometry::color_stride_floats(const Format& format) +{ + switch (format.vertex_layout) + { + case EVertexLayout::P3N3C4: { return 4; } + default: { assert(false); return 0; } + }; +} + +size_t GLModel::Geometry::color_offset_floats(const Format& format) +{ + switch (format.vertex_layout) + { + case EVertexLayout::P3N3C4: { return 6; } + default: { assert(false); return 0; } + }; +} + size_t GLModel::Geometry::index_stride_bytes(const Geometry& data) { switch (data.index_type) @@ -370,6 +425,7 @@ bool GLModel::Geometry::has_position(const Format& format) case EVertexLayout::P3T2: case EVertexLayout::P3N3: case EVertexLayout::P3N3T2: + case EVertexLayout::P3N3C4: case EVertexLayout::P4: { return true; } default: { assert(false); return false; } }; @@ -385,7 +441,8 @@ bool GLModel::Geometry::has_normal(const Format& format) case EVertexLayout::P3T2: case EVertexLayout::P4: { return false; } case EVertexLayout::P3N3: - case EVertexLayout::P3N3T2: { return true; } + case EVertexLayout::P3N3T2: + case EVertexLayout::P3N3C4: { return true; } default: { assert(false); return false; } }; } @@ -400,6 +457,23 @@ bool GLModel::Geometry::has_tex_coord(const Format& format) case EVertexLayout::P2: case EVertexLayout::P3: case EVertexLayout::P3N3: + case EVertexLayout::P3N3C4: + case EVertexLayout::P4: { return false; } + default: { assert(false); return false; } + }; +} + +bool GLModel::Geometry::has_color(const Format& format) +{ + switch (format.vertex_layout) + { + case EVertexLayout::P3N3C4: { return true; } + case EVertexLayout::P2: + case EVertexLayout::P2T2: + case EVertexLayout::P3: + case EVertexLayout::P3T2: + case EVertexLayout::P3N3: + case EVertexLayout::P3N3T2: case EVertexLayout::P4: { return false; } default: { assert(false); return false; } }; @@ -630,6 +704,7 @@ void GLModel::render(const std::pair& range, GLShaderProgram* sh const bool position = Geometry::has_position(data.format); const bool normal = Geometry::has_normal(data.format); const bool tex_coord = Geometry::has_tex_coord(data.format); + const bool color = Geometry::has_color(data.format); #if !SLIC3R_OPENGL_ES if (OpenGLManager::get_gl_info().is_core_profile()) { @@ -644,6 +719,7 @@ void GLModel::render(const std::pair& range, GLShaderProgram* sh int position_id = -1; int normal_id = -1; int tex_coord_id = -1; + int color_id = -1; if (position) { position_id = shader->get_attrib_location("v_position"); @@ -666,6 +742,13 @@ void GLModel::render(const std::pair& range, GLShaderProgram* sh glsafe(::glEnableVertexAttribArray(tex_coord_id)); } } + if (color) { + color_id = shader->get_attrib_location("v_color"); + if (color_id != -1) { + glsafe(::glVertexAttribPointer(color_id, Geometry::color_stride_floats(data.format), GL_FLOAT, GL_FALSE, vertex_stride_bytes, (const void*)Geometry::color_offset_bytes(data.format))); + glsafe(::glEnableVertexAttribArray(color_id)); + } + } shader->set_uniform("uniform_color", data.color); @@ -673,6 +756,8 @@ void GLModel::render(const std::pair& range, GLShaderProgram* sh glsafe(::glDrawElements(mode, range.second - range.first, index_type, (const void*)(range.first * Geometry::index_stride_bytes(data)))); glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + if (color_id != -1) + glsafe(::glDisableVertexAttribArray(color_id)); if (tex_coord_id != -1) glsafe(::glDisableVertexAttribArray(tex_coord_id)); if (normal_id != -1) diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index d007b31371..9b61afe4f0 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -43,6 +43,7 @@ namespace GUI { P3T2, // position 3 floats + texture coords 2 floats P3N3, // position 3 floats + normal 3 floats P3N3T2, // position 3 floats + normal 3 floats + texture coords 2 floats + P3N3C4, P4, // position 4 floats }; @@ -74,6 +75,7 @@ namespace GUI { void add_vertex(const Vec3f& position, const Vec2f& tex_coord); // EVertexLayout::P3T2 void add_vertex(const Vec3f& position, const Vec3f& normal); // EVertexLayout::P3N3 void add_vertex(const Vec3f& position, const Vec3f& normal, const Vec2f& tex_coord); // EVertexLayout::P3N3T2 + void add_vertex(const Vec3f& position, const Vec3f& normal, const ColorRGBA& color); void add_vertex(const Vec4f& position); // EVertexLayout::P4 void set_vertex(size_t id, const Vec3f& position, const Vec3f& normal); // EVertexLayout::P3N3 @@ -88,6 +90,7 @@ namespace GUI { Vec3f extract_position_3(size_t id) const; Vec3f extract_normal_3(size_t id) const; Vec2f extract_tex_coord_2(size_t id) const; + ColorRGBA extract_color_4(size_t id) const; unsigned int extract_index(size_t id) const; @@ -122,11 +125,16 @@ namespace GUI { static size_t tex_coord_offset_floats(const Format& format); static size_t tex_coord_offset_bytes(const Format& format) { return tex_coord_offset_floats(format) * sizeof(float); } + static size_t color_stride_floats(const Format& format); + static size_t color_offset_floats(const Format& format); + static size_t color_offset_bytes(const Format& format) { return color_offset_floats(format) * sizeof(float); } + static size_t index_stride_bytes(const Geometry& data); static bool has_position(const Format& format); static bool has_normal(const Format& format); static bool has_tex_coord(const Format& format); + static bool has_color(const Format& format); }; struct RenderData diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index 7f28d8f777..1ed6a63ce0 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -80,6 +80,10 @@ std::pair GLShadersManager::init() valid &= append_shader("variable_layer_height", { prefix + "variable_layer_height.vs", prefix + "variable_layer_height.fs" }); // used to render highlight contour around selected triangles inside the multi-material gizmo valid &= append_shader("mm_contour", { prefix + "mm_contour.vs", prefix + "mm_contour.fs" }); +#if !SLIC3R_OPENGL_ES + valid &= append_shader("painted_texture_preview", { prefix + "painted_texture_preview.vs", prefix + "painted_texture_preview.fs" }); + valid &= append_shader("painted_vertex_color_preview", { prefix + "painted_vertex_color_preview.vs", prefix + "painted_vertex_color_preview.fs" }); +#endif // !SLIC3R_OPENGL_ES // Used to render painted triangles inside the multi-material gizmo. Triangle normals are computed inside fragment shader. // For Apple's on Arm CPU computed triangle normals inside fragment shader using dFdx and dFdy has the opposite direction. // Because of this, objects had darker colors inside the multi-material gizmo. @@ -117,4 +121,3 @@ GLShaderProgram* GLShadersManager::get_current_shader() } } // namespace Slic3r - diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index 7bf4d008d4..baa3720b9b 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -173,6 +173,8 @@ bool GLTexture::load_from_svg_file(const std::string& filename, bool use_mipmaps bool GLTexture::load_from_raw_data(std::vector data, unsigned int w, unsigned int h, bool apply_anisotropy) { + reset(); + m_width = w; m_height = h; int n_pixels = m_width * m_height; diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 9b98052cdd..9c9776c6d7 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1,6 +1,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/TextureMapping.hpp" #include "libslic3r/Model.hpp" #include "GUI_Factories.hpp" @@ -37,7 +38,61 @@ static PrinterTechnology printer_technology() static int filaments_count() { - return wxGetApp().filaments_cnt(); + int count = wxGetApp().filaments_cnt(); + if (PresetBundle *bundle = wxGetApp().preset_bundle; bundle != nullptr) { + const ConfigOptionStrings *colors = bundle->project_config.option("filament_colour", false); + if (colors != nullptr) { + const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? + bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + bundle->texture_mapping_zones.load_entries(serialized, colors->values); + count = int(std::max(size_t(count), bundle->texture_mapping_zones.total_filaments(colors->values.size()))); + } + } + return count; +} + +static wxString texture_mapping_menu_label(const TextureMappingZone &zone) +{ + if (zone.is_2d_gradient()) + return _L("Texture Mapping 2D Gradient"); + const std::string color_model = TextureMappingManager::filament_color_mode_name(zone.filament_color_mode); + if (color_model == "any") + return _L("Texture Mapping"); + wxString color_model_text = from_u8(color_model); + color_model_text.MakeUpper(); + return _L("Texture Mapping ") + color_model_text; +} + +static wxString filament_menu_item_name(int filament_id_1based) +{ + if (filament_id_1based <= 0) + return _L("Default"); + PresetBundle *bundle = wxGetApp().preset_bundle; + if (bundle == nullptr) + return wxString::Format(_L("Filament %d"), filament_id_1based); + if (filament_id_1based <= int(bundle->filament_presets.size())) { + auto preset = bundle->filaments.find_preset(bundle->filament_presets[size_t(filament_id_1based - 1)]); + if (preset != nullptr) + return from_u8(preset->label(false)); + } + const TextureMappingZone *texture_zone = bundle->texture_mapping_zones.zone_from_id(unsigned(filament_id_1based)); + if (texture_zone != nullptr) + return texture_mapping_menu_label(*texture_zone); + return wxString::Format(_L("Filament %d"), filament_id_1based); +} + +static std::vector ui_ordered_filament_ids(int fallback_count) +{ + std::vector ids; + if (wxGetApp().plater() != nullptr) + ids = wxGetApp().plater()->sidebar().get_ui_ordered_filament_ids(); + if (ids.empty()) { + ids.reserve(size_t(std::max(0, fallback_count))); + for (int i = 1; i <= fallback_count; ++i) + ids.emplace_back(unsigned(i)); + } + return ids; } static bool is_improper_category(const std::string& category, const int filaments_cnt, const bool is_object_settings = true) @@ -981,21 +1036,23 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu) initial_extruder = config.has("extruder") ? config.extruder() : 1; } - for (int i = 0; i <= filaments_cnt; i++) + std::vector ordered_ids = ui_ordered_filament_ids(filaments_cnt); + std::vector menu_ids; + menu_ids.reserve(ordered_ids.size() + 1); + menu_ids.emplace_back(0); + for (unsigned int id : ordered_ids) + if (id <= unsigned(filaments_cnt)) + menu_ids.emplace_back(int(id)); + + for (int i : menu_ids) { bool is_active_extruder = i == initial_extruder; int icon_idx = i == 0 ? 0 : i - 1; wxString item_name = _L("Default"); - if (i > 0) { - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]); - if (preset == nullptr) { - item_name = wxString::Format(_L("Filament %d"), i); - } else { - item_name = from_u8(preset->label(false)); - } - } + if (i > 0) + item_name = filament_menu_item_name(i); if (is_active_extruder) { item_name << " (" + _L("current") + ")"; @@ -2175,7 +2232,16 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu) } } - for (int i = has_modifier ? 0 : 1; i <= filaments_cnt; i++) + std::vector ordered_ids = ui_ordered_filament_ids(filaments_cnt); + std::vector menu_ids; + menu_ids.reserve(ordered_ids.size() + (has_modifier ? 1 : 0)); + if (has_modifier) + menu_ids.emplace_back(0); + for (unsigned int id : ordered_ids) + if (id <= unsigned(filaments_cnt)) + menu_ids.emplace_back(int(id)); + + for (int i : menu_ids) { // BBS //bool is_active_extruder = i == initial_extruder; @@ -2183,14 +2249,8 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu) wxString item_name = _L("Default"); - if (i > 0) { - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]); - if (preset == nullptr) { - item_name = wxString::Format(_L("Filament %d"), i); - } else { - item_name = from_u8(preset->label(false)); - } - } + if (i > 0) + item_name = filament_menu_item_name(i); if (is_active_extruder) { item_name << " (" + _L("current") + ")"; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 726ea1aca3..6335a0d8a4 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -77,7 +77,18 @@ static DynamicPrintConfig& printer_config() static int filaments_count() { - return wxGetApp().filaments_cnt(); + int count = wxGetApp().filaments_cnt(); + if (PresetBundle *bundle = wxGetApp().preset_bundle; bundle != nullptr) { + const ConfigOptionStrings *colors = bundle->project_config.option("filament_colour", false); + if (colors != nullptr) { + const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? + bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + bundle->texture_mapping_zones.load_entries(serialized, colors->values); + count = int(std::max(size_t(count), bundle->texture_mapping_zones.total_filaments(colors->values.size()))); + } + } + return count; } static void take_snapshot(const std::string& snapshot_name) @@ -741,6 +752,29 @@ void ObjectList::update_filament_values_for_items(const size_t filaments_count) void ObjectList::update_filament_values_for_items_when_delete_filament(const size_t filament_id, const int replace_id) { int replace_filament_id = replace_id == -1 ? 1 : (replace_id + 1); + auto is_texture_mapping_zone = [](int filament_id_1based) { + return filament_id_1based > 0 && + wxGetApp().preset_bundle != nullptr && + wxGetApp().preset_bundle->texture_mapping_zones.is_texture_mapping_zone_id(unsigned(filament_id_1based)); + }; + auto remap_extruder = [filament_id, replace_filament_id, is_texture_mapping_zone](int extruder_id) { + if (extruder_id <= 0 || is_texture_mapping_zone(extruder_id)) + return extruder_id; + if (size_t(extruder_id) == filament_id + 1) + return replace_filament_id; + if (size_t(extruder_id) > filament_id + 1) + return extruder_id - 1; + return extruder_id; + }; + auto remap_optional_filament = [filament_id, is_texture_mapping_zone](int extruder_id) { + if (extruder_id <= 0 || is_texture_mapping_zone(extruder_id)) + return extruder_id; + if (size_t(extruder_id) == filament_id + 1) + return 0; + if (size_t(extruder_id) > filament_id + 1) + return extruder_id - 1; + return extruder_id; + }; for (size_t i = 0; i < m_objects->size(); ++i) { wxDataViewItem item = m_objects_model->GetItemById(i); if (!item) @@ -751,12 +785,8 @@ void ObjectList::update_filament_values_for_items_when_delete_filament(const siz if (!object->config.has("extruder")) { extruder = std::to_string(1); object->config.set_key_value("extruder", new ConfigOptionInt(1)); - } - else if (size_t(object->config.extruder()) == filament_id + 1) { - extruder = std::to_string(replace_filament_id); - object->config.set_key_value("extruder", new ConfigOptionInt(replace_filament_id)); } else { - int new_extruder = object->config.extruder() > filament_id ? object->config.extruder() - 1 : object->config.extruder(); + int new_extruder = remap_extruder(object->config.extruder()); extruder = wxString::Format("%d", new_extruder); object->config.set_key_value("extruder", new ConfigOptionInt(new_extruder)); } @@ -765,12 +795,11 @@ void ObjectList::update_filament_values_for_items_when_delete_filament(const siz static const char *keys[] = {"support_filament", "support_interface_filament"}; for (auto key : keys) { if (object->config.has(key)) { - if(object->config.opt_int(key) == filament_id + 1) + int new_value = remap_optional_filament(object->config.opt_int(key)); + if (new_value == 0) object->config.erase(key); - else { - int new_value = object->config.opt_int(key) > filament_id ? object->config.opt_int(key) - 1 : object->config.opt_int(key); + else object->config.set_key_value(key, new ConfigOptionInt(new_value)); - } } } @@ -782,23 +811,18 @@ void ObjectList::update_filament_values_for_items_when_delete_filament(const siz for (auto key : keys) { if (object->volumes[id]->config.has(key)) { - if (object->volumes[id]->config.opt_int(key) == filament_id + 1) + int new_value = remap_optional_filament(object->volumes[id]->config.opt_int(key)); + if (new_value == 0) object->volumes[id]->config.erase(key); - else { - int new_value = object->volumes[id]->config.opt_int(key) > filament_id ? object->volumes[id]->config.opt_int(key) - 1 : - object->volumes[id]->config.opt_int(key); - object->config.set_key_value(key, new ConfigOptionInt(new_value)); - } + else + object->volumes[id]->config.set_key_value(key, new ConfigOptionInt(new_value)); } } if (!object->volumes[id]->config.has("extruder")) { continue; - } - else if (size_t(object->volumes[id]->config.extruder()) == filament_id + 1) { - object->volumes[id]->config.set_key_value("extruder", new ConfigOptionInt(replace_filament_id)); } else { - int new_extruder = object->volumes[id]->config.extruder() > filament_id ? object->volumes[id]->config.extruder() - 1 : object->volumes[id]->config.extruder(); + int new_extruder = remap_extruder(object->volumes[id]->config.extruder()); extruder = wxString::Format("%d", new_extruder); object->volumes[id]->config.set_key_value("extruder", new ConfigOptionInt(new_extruder)); } diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index 71174665e1..8e00258bc3 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -6,6 +6,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/TextureMapping.hpp" //#include "libslic3r/Model.hpp" //#include "Plater.hpp" #include "Widgets/Label.hpp" @@ -29,6 +30,18 @@ static const int grid_cell_border_width = 2; static const int grid_cell_border_height = 2; static const int grid_cell_checkbox_size = 16; +static wxString texture_mapping_table_label(const TextureMappingZone &zone) +{ + if (zone.is_2d_gradient()) + return _L("Texture Mapping 2D Gradient"); + const std::string color_model = TextureMappingManager::filament_color_mode_name(zone.filament_color_mode); + if (color_model == "any") + return _L("Texture Mapping"); + wxString color_model_text = from_u8(color_model); + color_model_text.MakeUpper(); + return _L("Texture Mapping ") + color_model_text; +} + //min row count static const int g_min_row_count = 16; //when row count is bigger than overflow row count, will compute the total height by row_count*g_min_row_size @@ -2803,7 +2816,8 @@ int ObjectTablePanel::init_filaments_and_colors() //DynamicPrintConfig& global_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; const DynamicPrintConfig* global_config = m_plater->config(); const std::vector filament_presets = wxGetApp().preset_bundle->filament_presets; - m_filaments_count = filament_presets.size(); + const size_t physical_filaments_count = filament_presets.size(); + m_filaments_count = physical_filaments_count; if (m_filaments_count <= 0) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") %m_filaments_count; set_default_filaments_and_colors(); @@ -2815,18 +2829,27 @@ int ObjectTablePanel::init_filaments_and_colors() set_default_filaments_and_colors(); return -1; } + if (wxGetApp().preset_bundle != nullptr) { + std::vector colors = filament_opt->values; + colors.resize(physical_filaments_count, "#26A69A"); + const std::string serialized = wxGetApp().preset_bundle->project_config.has("texture_mapping_definitions") ? + wxGetApp().preset_bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + wxGetApp().preset_bundle->texture_mapping_zones.load_entries(serialized, colors); + m_filaments_count = std::max(m_filaments_count, int(wxGetApp().preset_bundle->texture_mapping_zones.total_filaments(physical_filaments_count))); + } m_filaments_colors.resize(m_filaments_count); m_filaments_name.resize(m_filaments_count); unsigned int color_count = filament_opt->values.size(); - if (color_count != m_filaments_count) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", invalid color count:%1%, extruder count: %2%") %color_count %m_filaments_count; + if (color_count != physical_filaments_count) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", invalid color count:%1%, extruder count: %2%") %color_count %physical_filaments_count; } unsigned int i = 0; ColorRGB rgb; while (i < m_filaments_count) { - const std::string& txt_color = global_config->opt_string("filament_colour", i); - if (i < color_count) { + if (i < physical_filaments_count) { + const std::string& txt_color = global_config->opt_string("filament_colour", i); if (decode_color(txt_color, rgb)) { m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar()); @@ -2835,14 +2858,24 @@ int ObjectTablePanel::init_filaments_and_colors() { m_filaments_colors[i] = *wxGREEN; } + m_filaments_name[i] = wxString(std::to_string(i+1) + ": " + filament_presets[i]); } else { - m_filaments_colors[i] = *wxGREEN; + const TextureMappingZone *zone = wxGetApp().preset_bundle != nullptr ? + wxGetApp().preset_bundle->texture_mapping_zones.zone_from_id(unsigned(i + 1)) : + nullptr; + if (zone != nullptr) { + if (decode_color(zone->display_color, rgb)) + m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar()); + else + m_filaments_colors[i] = wxColour("#8C8C8C"); + m_filaments_name[i] = wxString::Format("%d: %s", int(i + 1), texture_mapping_table_label(*zone)); + } else { + m_filaments_colors[i] = wxColour("#8C8C8C"); + m_filaments_name[i] = wxString::Format("%d: %s", int(i + 1), _L("Filament")); + } } - //parse the filaments - m_filaments_name[i] = wxString(std::to_string(i+1) + ": " + filament_presets[i]); - i++; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index e3fbfe0726..3b5c1465c9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -10,14 +10,23 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/ObjColorDialog.hpp" +#include "slic3r/GUI/Tab.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/TextureMapping.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "GLGizmoUtils.hpp" #include +#include +#include +#include +#include + namespace Slic3r::GUI { static inline void show_notification_extruders_limit_exceeded() @@ -30,6 +39,156 @@ static inline void show_notification_extruders_limit_exceeded() "first %1% filaments will be available in painting tool."), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)); } +static unsigned int ensure_texture_mapping_zone() +{ + if (wxGetApp().preset_bundle == nullptr || wxGetApp().plater() == nullptr) + return 0; + + TextureMappingManager &mgr = wxGetApp().preset_bundle->texture_mapping_zones; + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + std::vector physical_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + physical_colors.resize(num_physical, "#26A69A"); + + if (unsigned int existing_id = mgr.find_image_texture_zone_id(num_physical); existing_id != 0) { + if (TextureMappingZone *zone = mgr.zone_from_id(existing_id); + zone == nullptr || !TextureMappingManager::auto_adjust_texture_component_ids(*zone, num_physical, physical_colors)) { + return existing_id; + } + } else if (num_physical < 2) { + return 0; + } else { + mgr.ensure_image_texture_zone(num_physical, physical_colors); + } + + const std::string texture_serialized = mgr.serialize_entries(); + DynamicPrintConfig *print_cfg = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (ConfigOptionString *opt = print_cfg->option("texture_mapping_definitions")) + opt->value = texture_serialized; + else + print_cfg->set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_serialized)); + + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("texture_mapping_definitions")) + opt->value = texture_serialized; + else + wxGetApp().preset_bundle->project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_serialized)); + + wxGetApp().sidebar().update_texture_mapping_panel(false); + wxGetApp().sidebar().update_dynamic_filament_list(); + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe != nullptr) + wxGetApp().mainframe->on_config_changed(print_cfg); + + return mgr.find_image_texture_zone_id(num_physical); +} + +static bool model_volume_has_imported_image_texture_data(const ModelVolume *volume) +{ + return volume != nullptr && + !volume->imported_texture_rgba.empty() && + volume->imported_texture_width > 0 && + volume->imported_texture_height > 0; +} + +static bool model_volume_has_bakeable_image_texture_data(const ModelVolume *volume) +{ + if (!model_volume_has_imported_image_texture_data(volume)) + return false; + + const indexed_triangle_set &its = volume->mesh().its; + return !its.vertices.empty() && + !its.indices.empty() && + volume->imported_texture_uv_valid.size() == its.indices.size() && + volume->imported_texture_uvs_per_face.size() >= its.indices.size() * 6 && + volume->imported_texture_rgba.size() >= size_t(volume->imported_texture_width) * size_t(volume->imported_texture_height) * 4 && + std::any_of(volume->imported_texture_uv_valid.begin(), volume->imported_texture_uv_valid.end(), [](uint8_t valid) { + return valid != 0; + }); +} + +static float wrap_texture_uv_for_vertex_bake(float uv) +{ + if (!std::isfinite(uv)) + return 0.f; + + float wrapped = uv - std::floor(uv); + if (wrapped < 0.f) + wrapped += 1.f; + return wrapped; +} + +static ColorRGBA sample_texture_rgba_for_vertex_bake(const std::vector &rgba, + uint32_t width, + uint32_t height, + const Vec2f &uv) +{ + if (width == 0 || height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + + const float u = wrap_texture_uv_for_vertex_bake(uv.x()); + const float v = wrap_texture_uv_for_vertex_bake(uv.y()); + const float x = u * float(width > 1 ? width - 1 : 0); + const float y = v * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&rgba, width](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * 4 + channel; + return float(rgba[idx]) / 255.f; + }; + auto blend_channel = [&](size_t channel) { + const float c00 = sample_channel(x0, y0, channel); + const float c10 = sample_channel(x1, y0, channel); + const float c01 = sample_channel(x0, y1, channel); + const float c11 = sample_channel(x1, y1, channel); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + return std::clamp(cx0 + (cx1 - cx0) * ty, 0.f, 1.f); + }; + + return ColorRGBA(blend_channel(0), blend_channel(1), blend_channel(2), 1.f); +} + +static uint32_t pack_vertex_color_rgba(const ColorRGBA &color) +{ + auto to_u8 = [](float value) -> uint32_t { + return uint32_t(std::clamp(value, 0.f, 1.f) * 255.f + 0.5f); + }; + const uint32_t r = to_u8(color.r()); + const uint32_t g = to_u8(color.g()); + const uint32_t b = to_u8(color.b()); + const uint32_t a = to_u8(color.a()); + return (r << 24) | (g << 16) | (b << 8) | a; +} + +static bool barycentric_weights_for_region_vertex_colors(const Vec3f &point, + const Vec3f &p0, + const Vec3f &p1, + const Vec3f &p2, + Vec3f &weights) +{ + const Vec3f edge_0 = p1 - p0; + const Vec3f edge_1 = p2 - p0; + const Vec3f delta = point - p0; + const float d00 = edge_0.dot(edge_0); + const float d01 = edge_0.dot(edge_1); + const float d11 = edge_1.dot(edge_1); + const float d20 = delta.dot(edge_0); + const float d21 = delta.dot(edge_1); + const float denom = d00 * d11 - d01 * d01; + if (std::abs(denom) <= EPSILON) + return false; + + weights.y() = (d11 * d20 - d01 * d21) / denom; + weights.z() = (d00 * d21 - d01 * d20) / denom; + weights.x() = 1.f - weights.y() - weights.z(); + return std::isfinite(weights.x()) && std::isfinite(weights.y()) && std::isfinite(weights.z()); +} + void GLGizmoMmuSegmentation::on_opening() { if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) @@ -599,6 +758,59 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } ImGui::Separator(); + + const bool can_convert_regions_to_vertex_colors = selected_object_has_painted_regions(); + m_imgui->disabled_begin(!can_convert_regions_to_vertex_colors); + if (m_imgui->button(_L("Convert regions to vertex colors"))) + convert_selected_regions_to_vertex_colors(); + if (ImGui::IsItemHovered()) { + if (can_convert_regions_to_vertex_colors) + m_imgui->tooltip(_L("Convert painted color regions into imported vertex color data, clear the regions, and assign a texture mapping zone."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have painted color regions."), max_tooltip_width); + } + m_imgui->disabled_end(); + + const bool can_bake_image_texture_data = selected_object_has_bakeable_image_texture_data(); + m_imgui->disabled_begin(!can_bake_image_texture_data); + if (m_imgui->button(_L("Bake image texture to vertex colors"))) + bake_selected_object_image_texture_to_vertex_colors(); + if (ImGui::IsItemHovered()) { + if (can_bake_image_texture_data) + m_imgui->tooltip(_L("Sample imported image texture UVs into stored vertex colors, then discard the baked image texture data."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have imported image texture data with UVs."), max_tooltip_width); + } + m_imgui->disabled_end(); + + const bool can_clear_image_texture_data = selected_object_has_imported_texture_data(); + m_imgui->disabled_begin(!can_clear_image_texture_data); + if (m_imgui->button(_L("Clear Image Texture Data"))) + clear_selected_object_image_texture_data(); + if (ImGui::IsItemHovered()) { + if (can_clear_image_texture_data) + m_imgui->tooltip(_L("Discard imported image texture data from the selected object."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have imported image texture data."), max_tooltip_width); + } + m_imgui->disabled_end(); + + ImGui::Separator(); + + const bool can_apply_stored_vertex_colors = selected_object_has_imported_vertex_colors(); + m_imgui->disabled_begin(!can_apply_stored_vertex_colors); + if (m_imgui->button(_L("Convert vertex colors to regions (will erase painting)"))) + open_obj_vertex_color_mapping_dialog(); + if (ImGui::IsItemHovered()) { + if (can_apply_stored_vertex_colors) + m_imgui->tooltip(_L("Open OBJ color mapping dialog using stored imported vertex colors."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have stored imported vertex colors."), max_tooltip_width); + } + m_imgui->disabled_end(); + + ImGui::Separator(); + // ORCA: Remap filaments section (Border only, Title in border). // Styled as a panel for visual grouping. if (m_imgui->button(m_desc.at("perform_remap"))) { @@ -749,7 +961,7 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors() // This mesh does not account for the possible Z up SLA offset. const TriangleMesh* mesh = &mv->mesh(); - m_triangle_selectors.emplace_back(std::make_unique(*mesh, ebt_colors, 0.2)); + m_triangle_selectors.emplace_back(std::make_unique(*mesh, mv, ebt_colors, 0.2)); // Reset of TriangleSelector is done inside TriangleSelectorMmGUI's constructor, so we don't need it to perform it again in deserialize(). EnforcerBlockerType max_ebt = (EnforcerBlockerType)std::min(m_extruders_colors.size(), (size_t)EnforcerBlockerType::ExtruderMax); m_triangle_selectors.back()->deserialize(mv->mmu_segmentation_facets.get_data(), false, max_ebt); @@ -800,6 +1012,455 @@ void GLGizmoMmuSegmentation::tool_changed(wchar_t old_tool, wchar_t new_tool) } } +bool GLGizmoMmuSegmentation::selected_object_has_imported_vertex_colors() const +{ + const ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return false; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + if (!volume->imported_vertex_colors_rgba.empty()) + return true; + } + return false; +} + +bool GLGizmoMmuSegmentation::selected_object_has_imported_texture_data() const +{ + const ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return false; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + if (model_volume_has_imported_image_texture_data(volume)) + return true; + } + return false; +} + +bool GLGizmoMmuSegmentation::selected_object_has_bakeable_image_texture_data() const +{ + const ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return false; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + if (model_volume_has_bakeable_image_texture_data(volume)) + return true; + } + return false; +} + +bool GLGizmoMmuSegmentation::selected_object_has_painted_regions() const +{ + for (const auto &selector : m_triangle_selectors) { + if (selector == nullptr) + continue; + const TriangleSelector::TriangleSplittingData data = selector->serialize(); + for (size_t state_idx = static_cast(EnforcerBlockerType::Extruder1); state_idx < data.used_states.size(); ++state_idx) + if (data.used_states[state_idx]) + return true; + } + + const ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return false; + + for (const ModelVolume *volume : object->volumes) { + if (volume != nullptr && volume->is_model_part() && !volume->mmu_segmentation_facets.empty()) + return true; + } + return false; +} + +void GLGizmoMmuSegmentation::open_obj_vertex_color_mapping_dialog() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return; + + ModelVolume *target_volume = nullptr; + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + if (!volume->imported_vertex_colors_rgba.empty()) { + target_volume = volume; + break; + } + } + if (target_volume == nullptr) + return; + + if (target_volume->mesh().its.vertices.size() != target_volume->imported_vertex_colors_rgba.size()) + return; + + ObjDialogInOut in_out; + in_out.input_colors.reserve(target_volume->imported_vertex_colors_rgba.size()); + for (const uint32_t packed : target_volume->imported_vertex_colors_rgba) { + const float r = float((packed >> 24) & 0xFF) / 255.f; + const float g = float((packed >> 16) & 0xFF) / 255.f; + const float b = float((packed >> 8) & 0xFF) / 255.f; + const float a = float(packed & 0xFF) / 255.f; + in_out.input_colors.emplace_back(RGBA{r, g, b, a}); + } + + if (in_out.input_colors.empty()) + return; + + in_out.is_single_color = true; + const RGBA first_color = in_out.input_colors.front(); + for (const RGBA &color : in_out.input_colors) { + if (color != first_color) { + in_out.is_single_color = false; + break; + } + } + in_out.first_extruder_id = 1; + in_out.deal_vertex_color = true; + Model preview_model; + preview_model.add_object(*object); + in_out.model = &preview_model; + + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); + if (color_dlg.ShowModal() != wxID_OK) + return; + if (in_out.filament_ids.empty()) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert vertex colors to regions", UndoRedo::SnapshotType::GizmoAction); + if (!Model::obj_import_vertex_color_deal_for_object(in_out.filament_ids, in_out.first_extruder_id, object)) + return; + + update_from_model_object(); + m_parent.set_as_dirty(); + + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +void GLGizmoMmuSegmentation::bake_selected_object_image_texture_to_vertex_colors() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return; + + bool baked = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Bake image texture to vertex colors", UndoRedo::SnapshotType::GizmoAction); + + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part() || !model_volume_has_bakeable_image_texture_data(volume)) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + + struct VertexColorAccumulator + { + double r = 0.0; + double g = 0.0; + double b = 0.0; + double a = 0.0; + double weight = 0.0; + }; + + std::vector accumulators(its.vertices.size()); + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + if (volume->imported_texture_uv_valid[tri_idx] == 0) + continue; + + const auto &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + continue; + if (size_t(tri[0]) >= its.vertices.size() || + size_t(tri[1]) >= its.vertices.size() || + size_t(tri[2]) >= its.vertices.size()) + continue; + + const size_t uv_offset = tri_idx * 6; + const std::array uvs = { + Vec2f(volume->imported_texture_uvs_per_face[uv_offset + 0], volume->imported_texture_uvs_per_face[uv_offset + 1]), + Vec2f(volume->imported_texture_uvs_per_face[uv_offset + 2], volume->imported_texture_uvs_per_face[uv_offset + 3]), + Vec2f(volume->imported_texture_uvs_per_face[uv_offset + 4], volume->imported_texture_uvs_per_face[uv_offset + 5]) + }; + + const Vec3f p0 = its.vertices[size_t(tri[0])].cast(); + const Vec3f p1 = its.vertices[size_t(tri[1])].cast(); + const Vec3f p2 = its.vertices[size_t(tri[2])].cast(); + const float area = 0.5f * (p1 - p0).cross(p2 - p0).norm(); + const double weight = std::isfinite(area) && area > EPSILON ? double(area) : 1.0; + const std::array vertex_indices = {tri[0], tri[1], tri[2]}; + + for (size_t corner = 0; corner < 3; ++corner) { + const ColorRGBA color = sample_texture_rgba_for_vertex_bake(volume->imported_texture_rgba, + volume->imported_texture_width, + volume->imported_texture_height, + uvs[corner]); + VertexColorAccumulator &acc = accumulators[size_t(vertex_indices[corner])]; + acc.r += double(color.r()) * weight; + acc.g += double(color.g()) * weight; + acc.b += double(color.b()) * weight; + acc.a += double(color.a()) * weight; + acc.weight += weight; + } + } + + std::vector vertex_colors; + vertex_colors.reserve(its.vertices.size()); + for (size_t vertex_idx = 0; vertex_idx < its.vertices.size(); ++vertex_idx) { + const VertexColorAccumulator &acc = accumulators[vertex_idx]; + if (acc.weight > 0.0) { + vertex_colors.emplace_back(pack_vertex_color_rgba(ColorRGBA(float(acc.r / acc.weight), + float(acc.g / acc.weight), + float(acc.b / acc.weight), + float(acc.a / acc.weight)))); + } else if (vertex_idx < volume->imported_vertex_colors_rgba.size()) { + vertex_colors.emplace_back(volume->imported_vertex_colors_rgba[vertex_idx]); + } else { + vertex_colors.emplace_back(pack_vertex_color_rgba(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + } + } + + if (vertex_colors.size() != its.vertices.size()) + continue; + + volume->imported_vertex_colors_rgba = std::move(vertex_colors); + volume->imported_texture_uvs_per_face.clear(); + volume->imported_texture_uv_valid.clear(); + volume->imported_texture_rgba.clear(); + volume->imported_texture_width = 0; + volume->imported_texture_height = 0; + baked = true; + } + + if (!baked) + return; + + for (auto &selector : m_triangle_selectors) + if (selector != nullptr) + selector->request_update_render_data(true); + + m_parent.set_as_dirty(); + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +void GLGizmoMmuSegmentation::convert_selected_regions_to_vertex_colors() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr || m_triangle_selectors.empty()) + return; + + std::vector color_strings; + if (wxGetApp().plater() != nullptr) + color_strings = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + + std::vector filament_colors; + filament_colors.reserve(color_strings.size()); + for (const std::string &color_string : color_strings) { + unsigned char rgba[4] = {38, 166, 154, 255}; + BitmapCache::parse_color4(color_string, rgba); + filament_colors.emplace_back(float(rgba[0]) / 255.f, + float(rgba[1]) / 255.f, + float(rgba[2]) / 255.f, + float(rgba[3]) / 255.f); + } + if (filament_colors.empty()) + filament_colors.emplace_back(0.15f, 0.65f, 0.6f, 1.f); + + auto color_for_filament_id = [&filament_colors](unsigned int filament_id) { + if (filament_id >= 1 && filament_id <= filament_colors.size()) + return filament_colors[filament_id - 1]; + return filament_colors.front(); + }; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert regions to vertex colors", UndoRedo::SnapshotType::GizmoAction); + + bool converted = false; + int selector_idx = -1; + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + ++selector_idx; + if (selector_idx < 0 || size_t(selector_idx) >= m_triangle_selectors.size() || m_triangle_selectors[size_t(selector_idx)] == nullptr) + continue; + + const auto &its = volume->mesh().its; + if (its.vertices.empty() || its.indices.empty()) + continue; + + std::vector> triangles_per_type; + m_triangle_selectors[size_t(selector_idx)]->get_facet_triangles(triangles_per_type); + if (triangles_per_type.empty()) + continue; + + struct VertexColorAccumulator + { + double r = 0.0; + double g = 0.0; + double b = 0.0; + double a = 0.0; + double weight = 0.0; + }; + + std::vector accumulators(its.vertices.size()); + bool accumulated_any = false; + const unsigned int base_filament_id = volume->extruder_id() > 0 ? unsigned(volume->extruder_id()) : 1u; + + for (size_t state_idx = 0; state_idx < triangles_per_type.size(); ++state_idx) { + const unsigned int filament_id = state_idx == 0 ? base_filament_id : unsigned(state_idx); + ColorRGBA state_color = color_for_filament_id(filament_id); + state_color.a(1.f); + + for (const TriangleSelector::FacetStateTriangle &triangle : triangles_per_type[state_idx]) { + if (triangle.source_triangle < 0) + continue; + const size_t source_triangle = size_t(triangle.source_triangle); + if (source_triangle >= its.indices.size()) + continue; + + const auto &source_indices = its.indices[source_triangle]; + if (source_indices[0] < 0 || source_indices[1] < 0 || source_indices[2] < 0) + continue; + if (size_t(source_indices[0]) >= its.vertices.size() || + size_t(source_indices[1]) >= its.vertices.size() || + size_t(source_indices[2]) >= its.vertices.size()) + continue; + + const Vec3f source_p0 = its.vertices[size_t(source_indices[0])].cast(); + const Vec3f source_p1 = its.vertices[size_t(source_indices[1])].cast(); + const Vec3f source_p2 = its.vertices[size_t(source_indices[2])].cast(); + const Vec3f centroid = (triangle.vertices[0] + triangle.vertices[1] + triangle.vertices[2]) / 3.f; + Vec3f weights(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + if (!barycentric_weights_for_region_vertex_colors(centroid, source_p0, source_p1, source_p2, weights)) + weights = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + + weights.x() = std::max(0.f, weights.x()); + weights.y() = std::max(0.f, weights.y()); + weights.z() = std::max(0.f, weights.z()); + const float weights_sum = weights.x() + weights.y() + weights.z(); + if (weights_sum > EPSILON) + weights /= weights_sum; + else + weights = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + + const float area = 0.5f * (triangle.vertices[1] - triangle.vertices[0]).cross(triangle.vertices[2] - triangle.vertices[0]).norm(); + const double area_weight = std::max(double(area), 1e-6); + const std::array bary = {weights.x(), weights.y(), weights.z()}; + + for (size_t corner = 0; corner < 3; ++corner) { + VertexColorAccumulator &acc = accumulators[size_t(source_indices[corner])]; + const double weight = area_weight * double(bary[corner]); + acc.r += double(state_color.r()) * weight; + acc.g += double(state_color.g()) * weight; + acc.b += double(state_color.b()) * weight; + acc.a += double(state_color.a()) * weight; + acc.weight += weight; + } + accumulated_any = true; + } + } + + if (!accumulated_any) + continue; + + const ColorRGBA fallback_color = color_for_filament_id(base_filament_id); + std::vector vertex_colors; + vertex_colors.reserve(its.vertices.size()); + for (const VertexColorAccumulator &acc : accumulators) { + if (acc.weight > 0.0) { + vertex_colors.emplace_back(pack_vertex_color_rgba(ColorRGBA(float(acc.r / acc.weight), + float(acc.g / acc.weight), + float(acc.b / acc.weight), + float(acc.a / acc.weight)))); + } else { + vertex_colors.emplace_back(pack_vertex_color_rgba(fallback_color)); + } + } + + volume->imported_vertex_colors_rgba = std::move(vertex_colors); + volume->mmu_segmentation_facets.reset(); + m_triangle_selectors[size_t(selector_idx)]->reset(); + m_triangle_selectors[size_t(selector_idx)]->request_update_render_data(true); + converted = true; + } + + if (!converted) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + update_from_model_object(); + m_parent.update_volumes_colors_by_extruder(); + m_parent.set_as_dirty(); + + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +void GLGizmoMmuSegmentation::clear_selected_object_image_texture_data() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return; + + bool cleared = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Clear image texture data", UndoRedo::SnapshotType::GizmoAction); + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part() || !model_volume_has_imported_image_texture_data(volume)) + continue; + + volume->imported_texture_uvs_per_face.clear(); + volume->imported_texture_uv_valid.clear(); + volume->imported_texture_rgba.clear(); + volume->imported_texture_width = 0; + volume->imported_texture_height = 0; + cleared = true; + } + + if (!cleared) + return; + + for (auto &selector : m_triangle_selectors) + if (selector != nullptr) + selector->request_update_render_data(true); + + m_parent.set_as_dirty(); + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + PainterGizmoType GLGizmoMmuSegmentation::get_painter_type() const { return PainterGizmoType::MM_SEGMENTATION; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 756ada7670..209340a9cd 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -143,6 +143,14 @@ private: // Filament remapping methods void remap_filament_assignments(); void render_filament_remap_ui(float window_width, float max_tooltip_width); + bool selected_object_has_imported_vertex_colors() const; + bool selected_object_has_imported_texture_data() const; + bool selected_object_has_bakeable_image_texture_data() const; + bool selected_object_has_painted_regions() const; + void open_obj_vertex_color_mapping_dialog(); + void bake_selected_object_image_texture_to_vertex_colors(); + void convert_selected_regions_to_vertex_colors(); + void clear_selected_object_image_texture_data(); // ORCA: Helper to update the cache of used filaments void update_used_filaments(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 8f0cd9a43d..51be765d95 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -1,5 +1,6 @@ #include "GLGizmoPainterBase.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/MMUPaintedTexturePreview.hpp" #include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" #include @@ -18,6 +19,21 @@ namespace Slic3r::GUI { +namespace { + +bool model_volume_has_texture_preview_data_for_painting(const ModelVolume &model_volume) +{ + return !model_volume.imported_texture_rgba.empty() && + model_volume.imported_texture_width > 0 && + model_volume.imported_texture_height > 0 && + model_volume.imported_texture_uv_valid.size() == model_volume.mesh().its.indices.size() && + model_volume.imported_texture_uvs_per_face.size() >= model_volume.mesh().its.indices.size() * 6 && + model_volume.imported_texture_rgba.size() >= + size_t(model_volume.imported_texture_width) * size_t(model_volume.imported_texture_height) * 4; +} + +} // namespace + std::shared_ptr GLGizmoPainterBase::s_sphere = nullptr; GLGizmoPainterBase::GLGizmoPainterBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) @@ -125,6 +141,12 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); shader->set_uniform("slope.normal_z", normal_z); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); + m_triangle_selectors[mesh_id]->render_texture_preview(trafo_matrix, + view_matrix, + camera.get_projection_matrix(), + clp_data.z_range, + clp_data.clp_dataf); + shader->start_using(); if (is_left_handed) glsafe(::glFrontFace(GL_CCW)); @@ -1586,6 +1608,41 @@ void TriangleSelectorPatch::update_render_data() update_triangles_per_type(); this->finalize_triangle_indices(); + m_texture_preview_models.clear(); + m_texture_preview_colors.clear(); + m_texture_preview_filament_ids.clear(); + m_vertex_color_preview_models.clear(); + m_vertex_color_preview_colors.clear(); + m_vertex_color_preview_filament_ids.clear(); + if (m_model_volume != nullptr) { + std::vector> triangles_per_type; + get_facet_triangles(triangles_per_type); + const size_t num_physical = std::max(0, wxGetApp().filaments_cnt()); + const TextureMappingManager *texture_mgr = wxGetApp().preset_bundle != nullptr ? + &wxGetApp().preset_bundle->texture_mapping_zones : nullptr; + m_texture_preview_visual_signature = texture_preview_settings_signature(num_physical, texture_mgr); + if (model_volume_has_texture_preview_data_for_painting(*m_model_volume)) { + build_mmu_texture_preview_models(*m_model_volume, + triangles_per_type, + m_ebt_colors, + m_model_volume->extruder_id() > 0 ? unsigned(m_model_volume->extruder_id()) : 0u, + num_physical, + texture_mgr, + m_texture_preview_models, + m_texture_preview_colors, + m_texture_preview_filament_ids); + } + build_mmu_vertex_color_preview_models(*m_model_volume, + triangles_per_type, + m_ebt_colors, + m_model_volume->extruder_id() > 0 ? unsigned(m_model_volume->extruder_id()) : 0u, + num_physical, + texture_mgr, + m_vertex_color_preview_models, + m_vertex_color_preview_colors, + m_vertex_color_preview_filament_ids); + } + m_paint_changed = false; } @@ -1683,10 +1740,78 @@ void TriangleSelectorPatch::release_geometry() triangle_indices_VBO_id = 0; } this->clear(); + m_texture_preview_models.clear(); + m_texture_preview_colors.clear(); + m_texture_preview_filament_ids.clear(); + m_texture_preview.reset(); + m_texture_preview_signature = 0; + m_texture_preview_visual_signature = 0; + m_vertex_color_preview_models.clear(); + m_vertex_color_preview_colors.clear(); + m_vertex_color_preview_filament_ids.clear(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: released geometry")%__LINE__; } +void TriangleSelectorPatch::render_texture_preview(const Transform3d& matrix, + const Transform3d& view_matrix, + const Transform3d& projection_matrix, + const std::array& z_range, + const std::array& clipping_plane) const +{ + if (m_model_volume == nullptr) + return; + + const size_t num_physical = std::max(0, wxGetApp().filaments_cnt()); + const TextureMappingManager *texture_mgr = wxGetApp().preset_bundle != nullptr ? + &wxGetApp().preset_bundle->texture_mapping_zones : nullptr; + const size_t current_visual_signature = texture_preview_settings_signature(num_physical, texture_mgr); + if (current_visual_signature != m_texture_preview_visual_signature) { + TriangleSelectorPatch *self = const_cast(this); + self->request_update_render_data(true); + self->update_render_data(); + } + + if (m_texture_preview_models.empty() && m_vertex_color_preview_models.empty()) + return; + + auto adjusted_preview_colors = [](const std::vector &colors) { + std::vector preview_colors = colors; + for (ColorRGBA &preview_color : preview_colors) + preview_color = adjust_color_for_rendering(preview_color); + return preview_colors; + }; + + if (!m_texture_preview_models.empty() && + ensure_model_volume_texture_preview(*m_model_volume, m_texture_preview, m_texture_preview_signature)) { + render_model_texture_preview_models(m_texture_preview_models, + adjusted_preview_colors(m_texture_preview_colors), + m_texture_preview_filament_ids, + num_physical, + texture_mgr, + *m_model_volume, + m_texture_preview, + matrix, + view_matrix, + projection_matrix, + z_range, + clipping_plane); + } + + if (!m_vertex_color_preview_models.empty()) { + render_model_vertex_color_preview_models(m_vertex_color_preview_models, + adjusted_preview_colors(m_vertex_color_preview_colors), + m_vertex_color_preview_filament_ids, + num_physical, + texture_mgr, + matrix, + view_matrix, + projection_matrix, + z_range, + clipping_plane); + } +} + void TriangleSelectorPatch::finalize_vertices() { /*assert(m_vertices_VBO_id == 0); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 8ec1cb1fb0..9e2945afae 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -4,6 +4,7 @@ #include "GLGizmoBase.hpp" #include "slic3r/GUI/GLModel.hpp" +#include "slic3r/GUI/GLTexture.hpp" #include "libslic3r/ObjectID.hpp" #include "libslic3r/TriangleSelector.hpp" @@ -12,6 +13,7 @@ #include #include +#include #include @@ -37,6 +39,11 @@ public: virtual ~TriangleSelectorGUI() = default; virtual void render(ImGuiWrapper* imgui, const Transform3d& matrix); + virtual void render_texture_preview(const Transform3d& matrix, + const Transform3d& view_matrix, + const Transform3d& projection_matrix, + const std::array& z_range, + const std::array& clipping_plane) const {} //void render(const Transform3d& matrix) { this->render(nullptr, matrix); } void set_wireframe_needed(bool need_wireframe) { m_need_wireframe = need_wireframe; } bool get_wireframe_needed() { return m_need_wireframe; } @@ -101,11 +108,18 @@ class TriangleSelectorPatch : public TriangleSelectorGUI { public: explicit TriangleSelectorPatch(const TriangleMesh& mesh, const std::vector ebt_colors, float edge_limit = 0.6f) : TriangleSelectorGUI(mesh, edge_limit), m_ebt_colors(ebt_colors) {} + explicit TriangleSelectorPatch(const TriangleMesh& mesh, const ModelVolume* model_volume, const std::vector ebt_colors, float edge_limit = 0.6f) + : TriangleSelectorGUI(mesh, edge_limit), m_model_volume(model_volume), m_ebt_colors(ebt_colors) {} virtual ~TriangleSelectorPatch() = default; // Render current selection. Transformation matrices are supposed // to be already set. void render(ImGuiWrapper* imgui, const Transform3d& matrix) override; + void render_texture_preview(const Transform3d& matrix, + const Transform3d& view_matrix, + const Transform3d& projection_matrix, + const std::array& z_range, + const std::array& clipping_plane) const override; // TriangleSelector.m_triangles => m_gizmo_scene.triangle_patches void update_triangles_per_type(); // m_gizmo_scene.triangle_patches => TriangleSelector.m_triangles @@ -167,7 +181,17 @@ protected: std::vector m_vertices_VBO_ids; std::vector m_triangle_indices_VBO_ids; + const ModelVolume* m_model_volume { nullptr }; std::vector m_ebt_colors; + mutable std::vector m_texture_preview_models; + std::vector m_texture_preview_colors; + std::vector m_texture_preview_filament_ids; + mutable GLTexture m_texture_preview; + mutable size_t m_texture_preview_signature { 0 }; + size_t m_texture_preview_visual_signature { 0 }; + mutable std::vector m_vertex_color_preview_models; + std::vector m_vertex_color_preview_colors; + std::vector m_vertex_color_preview_filament_ids; bool m_filter_state = false; diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp new file mode 100644 index 0000000000..75ff72a6d1 --- /dev/null +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -0,0 +1,2159 @@ +#include + +#include "MMUPaintedTexturePreview.hpp" + +#include "3DScene.hpp" +#include "BitmapCache.hpp" +#include "GLShader.hpp" +#include "GUI_App.hpp" + +#include "libslic3r/Config.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/TextureMapping.hpp" +#include "libslic3r/filament_mixer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +constexpr float k_preview_offset = 0.001f; +constexpr float k_polygon_offset_factor = -1.f; +constexpr float k_polygon_offset_units = -1.f; +constexpr float k_epsilon = 1e-6f; +constexpr unsigned int k_simulated_texture_preview_max_edge = 1024; +constexpr size_t k_simulated_texture_preview_max_pixels = 1024ull * 1024ull; + +struct TexturePreviewMixCandidate +{ + std::array rgb; + std::vector weights; +}; + +struct TexturePreviewSimulationSettings +{ + int mapping_mode = int(TextureMappingZone::TextureMappingFilamentBlending); + int filament_color_mode = TextureMappingZone::DefaultFilamentColorMode; + bool force_sequential_filaments = false; + bool limit_texture_resolution = true; + bool compact_offset_mode = false; + float contrast_pct = 100.f; + float tone_gamma = 1.f; + std::vector component_ids; + std::vector> component_colors; + std::vector component_strength_factors; + std::vector semantic_component_indices; + std::vector generic_mix_candidates; +}; + +struct SurfaceGradientPreviewSettings +{ + std::vector component_ids; + std::vector> component_colors; + std::vector distances_mm; + std::vector angles_deg; + std::vector strength_factors; + std::vector minimum_offset_factors; + float max_component_distance_mm = 0.f; + float max_width_delta_limit_mm = 0.f; + float sagging_ratio = 0.f; + int angle_mode = int(TextureMappingZone::OffsetAngleObjectCenter); + bool rotation_enabled = true; + float rotations = 1.f; + float repeats = 1.f; + bool reverse_repeats = true; + bool clockwise = true; + int fade_mode = int(TextureMappingZone::OffsetFadeNone); + bool limit_texture_resolution = true; + Vec3f center = Vec3f::Zero(); + float z_min = 0.f; + float z_max = 0.f; +}; + +struct TexturePreviewSimulationResult +{ + size_t signature { 0 }; + unsigned int width { 0 }; + unsigned int height { 0 }; + std::vector rgba; +}; + +struct TexturePreviewSimulationCacheEntry +{ + std::unique_ptr texture; + size_t uploaded_signature { 0 }; + size_t pending_signature { 0 }; + std::future pending_future; +}; + +bool model_volume_has_texture_preview_data(const ModelVolume &model_volume) +{ + return !model_volume.imported_texture_rgba.empty() && + model_volume.imported_texture_width > 0 && + model_volume.imported_texture_height > 0 && + model_volume.imported_texture_uv_valid.size() == model_volume.mesh().its.indices.size() && + model_volume.imported_texture_uvs_per_face.size() >= model_volume.mesh().its.indices.size() * 6 && + model_volume.imported_texture_rgba.size() >= + size_t(model_volume.imported_texture_width) * size_t(model_volume.imported_texture_height) * 4; +} + +bool model_volume_has_vertex_color_preview_data(const ModelVolume &model_volume) +{ + return !model_volume.imported_vertex_colors_rgba.empty() && + model_volume.imported_vertex_colors_rgba.size() == model_volume.mesh().its.vertices.size(); +} + +std::array unwrap_triangle_uvs(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2) +{ + std::array out{ uv0, uv1, uv2 }; + + auto unwrap_axis = [&out](bool use_u_axis) { + std::array values = { + use_u_axis ? out[0].x() : out[0].y(), + use_u_axis ? out[1].x() : out[1].y(), + use_u_axis ? out[2].x() : out[2].y() + }; + const float value_min = std::min({ values[0], values[1], values[2] }); + const float value_max = std::max({ values[0], values[1], values[2] }); + if (value_max - value_min <= 0.5f) + return; + + for (float &value : values) + if (value < 0.5f) + value += 1.f; + + if (use_u_axis) { + out[0].x() = values[0]; + out[1].x() = values[1]; + out[2].x() = values[2]; + } else { + out[0].y() = values[0]; + out[1].y() = values[1]; + out[2].y() = values[2]; + } + }; + + unwrap_axis(true); + unwrap_axis(false); + return out; +} + +bool barycentric_weights(const Vec3f &point, const Vec3f &p0, const Vec3f &p1, const Vec3f &p2, Vec3f &weights) +{ + const Vec3f edge_0 = p1 - p0; + const Vec3f edge_1 = p2 - p0; + const Vec3f delta = point - p0; + const float d00 = edge_0.dot(edge_0); + const float d01 = edge_0.dot(edge_1); + const float d11 = edge_1.dot(edge_1); + const float d20 = delta.dot(edge_0); + const float d21 = delta.dot(edge_1); + const float denom = d00 * d11 - d01 * d01; + if (std::abs(denom) <= k_epsilon) + return false; + + weights.y() = (d11 * d20 - d01 * d21) / denom; + weights.z() = (d00 * d21 - d01 * d20) / denom; + weights.x() = 1.f - weights.y() - weights.z(); + return std::isfinite(weights.x()) && std::isfinite(weights.y()) && std::isfinite(weights.z()); +} + +ColorRGBA unpack_vertex_color(uint32_t packed) +{ + return { + float((packed >> 24) & 0xFF) / 255.f, + float((packed >> 16) & 0xFF) / 255.f, + float((packed >> 8) & 0xFF) / 255.f, + float(packed & 0xFF) / 255.f + }; +} + +ColorRGBA interpolate_color(const std::array &colors, const Vec3f &weights) +{ + Vec3f clamped(std::max(0.f, weights.x()), std::max(0.f, weights.y()), std::max(0.f, weights.z())); + const float sum = clamped.x() + clamped.y() + clamped.z(); + if (sum > k_epsilon) + clamped /= sum; + else + clamped = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + + return { + colors[0].r() * clamped.x() + colors[1].r() * clamped.y() + colors[2].r() * clamped.z(), + colors[0].g() * clamped.x() + colors[1].g() * clamped.y() + colors[2].g() * clamped.z(), + colors[0].b() * clamped.x() + colors[1].b() * clamped.y() + colors[2].b() * clamped.z(), + colors[0].a() * clamped.x() + colors[1].a() * clamped.y() + colors[2].a() * clamped.z() + }; +} + +std::array decode_color(const std::string &color) +{ + unsigned char rgba[4] = { 38, 166, 154, 255 }; + GUI::BitmapCache::parse_color4(color, rgba); + return { + float(rgba[0]) / 255.f, + float(rgba[1]) / 255.f, + float(rgba[2]) / 255.f + }; +} + +ColorRGBA blend_component_colors(const std::vector> &colors, const std::vector &weights) +{ + if (colors.empty() || weights.empty()) + return { 0.15f, 0.65f, 0.6f, 1.f }; + + float total = 0.f; + for (size_t idx = 0; idx < std::min(colors.size(), weights.size()); ++idx) + total += std::max(0.f, weights[idx]); + if (total <= k_epsilon) + return { colors.front()[0], colors.front()[1], colors.front()[2], 1.f }; + + float out_r = colors.front()[0]; + float out_g = colors.front()[1]; + float out_b = colors.front()[2]; + float accumulated = std::max(0.f, weights[0]); + if (accumulated <= k_epsilon) { + for (size_t idx = 1; idx < std::min(colors.size(), weights.size()); ++idx) { + if (weights[idx] > k_epsilon) { + out_r = colors[idx][0]; + out_g = colors[idx][1]; + out_b = colors[idx][2]; + accumulated = weights[idx]; + break; + } + } + } + + for (size_t idx = 1; idx < std::min(colors.size(), weights.size()); ++idx) { + const float weight = std::max(0.f, weights[idx]); + if (weight <= k_epsilon) + continue; + const float t = weight / std::max(accumulated + weight, k_epsilon); + filament_mixer_lerp_float(out_r, out_g, out_b, colors[idx][0], colors[idx][1], colors[idx][2], t, &out_r, &out_g, &out_b); + accumulated += weight; + } + + return { std::clamp(out_r, 0.f, 1.f), std::clamp(out_g, 0.f, 1.f), std::clamp(out_b, 0.f, 1.f), 1.f }; +} + +float clamp01(float value) +{ + return std::clamp(value, 0.f, 1.f); +} + +unsigned char to_u8(float value) +{ + return static_cast(clamp01(value) * 255.f + 0.5f); +} + +void make_texture_preview_rgba_opaque(std::vector &rgba) +{ + for (size_t idx = 3; idx < rgba.size(); idx += 4) + rgba[idx] = 255; +} + +void configure_texture_preview_sampler(const GUI::GLTexture &texture) +{ + if (texture.get_id() == 0) + return; + + glsafe(::glBindTexture(GL_TEXTURE_2D, texture.get_id())); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); +} + +std::array limited_simulated_texture_preview_size(unsigned int width, unsigned int height) +{ + if (width == 0 || height == 0) + return { 0, 0 }; + + double scale = 1.0; + const unsigned int max_edge = std::max(width, height); + if (max_edge > k_simulated_texture_preview_max_edge) + scale = std::min(scale, double(k_simulated_texture_preview_max_edge) / double(max_edge)); + + const double pixel_count = double(width) * double(height); + if (pixel_count * scale * scale > double(k_simulated_texture_preview_max_pixels)) + scale = std::min(scale, std::sqrt(double(k_simulated_texture_preview_max_pixels) / pixel_count)); + + if (scale >= 1.0) + return { width, height }; + + unsigned int limited_width = std::max(1u, unsigned(std::lround(double(width) * scale))); + unsigned int limited_height = std::max(1u, unsigned(std::lround(double(height) * scale))); + while (limited_width > k_simulated_texture_preview_max_edge || + limited_height > k_simulated_texture_preview_max_edge || + size_t(limited_width) * size_t(limited_height) > k_simulated_texture_preview_max_pixels) { + if (limited_width >= limited_height && limited_width > 1) + --limited_width; + else if (limited_height > 1) + --limited_height; + else + break; + } + return { limited_width, limited_height }; +} + +std::array sample_texture_preview_rgb_bilinear(const std::vector &rgba, + unsigned int width, + unsigned int height, + unsigned int preview_x, + unsigned int preview_y, + unsigned int preview_width, + unsigned int preview_height) +{ + const double src_x = std::clamp((double(preview_x) + 0.5) * double(width) / double(std::max(1u, preview_width)) - 0.5, + 0.0, + double(width - 1)); + const double src_y = std::clamp((double(preview_y) + 0.5) * double(height) / double(std::max(1u, preview_height)) - 0.5, + 0.0, + double(height - 1)); + const unsigned int x0 = std::min(width - 1, unsigned(std::floor(src_x))); + const unsigned int y0 = std::min(height - 1, unsigned(std::floor(src_y))); + const unsigned int x1 = std::min(width - 1, x0 + 1); + const unsigned int y1 = std::min(height - 1, y0 + 1); + const double tx = src_x - double(x0); + const double ty = src_y - double(y0); + + auto channel_at = [&rgba, width](unsigned int x, unsigned int y, size_t channel) { + return double(rgba[(size_t(y) * size_t(width) + size_t(x)) * 4 + channel]); + }; + auto sample_channel = [&](size_t channel) { + const double top = channel_at(x0, y0, channel) * (1.0 - tx) + channel_at(x1, y0, channel) * tx; + const double bottom = channel_at(x0, y1, channel) * (1.0 - tx) + channel_at(x1, y1, channel) * tx; + return static_cast(std::clamp(int(std::lround(top * (1.0 - ty) + bottom * ty)), 0, 255)); + }; + + return { sample_channel(0), sample_channel(1), sample_channel(2) }; +} + +unsigned int texture_preview_rgb_cache_key(const std::array &rgb, bool quantize) +{ + if (quantize) + return unsigned(rgb[0] >> 3) | (unsigned(rgb[1] >> 3) << 5) | (unsigned(rgb[2] >> 3) << 10); + return unsigned(rgb[0]) | (unsigned(rgb[1]) << 8) | (unsigned(rgb[2]) << 16); +} + +unsigned int filament_id_for_state(size_t state_id, unsigned int base_filament_id) +{ + return state_id == 0 ? base_filament_id : unsigned(state_id); +} + +const TextureMappingZone *zone_for_filament(unsigned int filament_id, size_t num_physical, const TextureMappingManager *texture_mgr) +{ + return texture_mgr != nullptr && filament_id > num_physical ? texture_mgr->zone_from_id(filament_id) : nullptr; +} + +bool is_image_zone(const TextureMappingZone &zone) +{ + return zone.enabled && !zone.deleted && zone.is_image_texture(); +} + +bool is_gradient_zone(const TextureMappingZone &zone) +{ + return zone.enabled && !zone.deleted && zone.is_2d_gradient(); +} + +float texture_preview_mix_for_filament(unsigned int filament_id, size_t num_physical, const TextureMappingManager *texture_mgr) +{ + const TextureMappingZone *zone = zone_for_filament(filament_id, num_physical, texture_mgr); + if (zone == nullptr || (!is_image_zone(*zone) && !is_gradient_zone(*zone))) + return 0.f; + return std::clamp(zone->preview_opacity_pct, 0.f, 100.f) / 100.f; +} + +bool texture_preview_settings_invalid_for_filament(unsigned int filament_id, size_t num_physical, const TextureMappingManager *texture_mgr) +{ + const TextureMappingZone *zone = zone_for_filament(filament_id, num_physical, texture_mgr); + if (zone == nullptr) + return false; + if (is_image_zone(*zone)) + return TextureMappingManager::component_count_mismatch(*zone, num_physical); + if (is_gradient_zone(*zone)) + return TextureMappingManager::selected_component_ids(*zone, num_physical).size() < 2; + return false; +} + +std::vector physical_filament_colors_for_texture_preview(size_t num_physical) +{ + std::vector colors; + if (GUI::wxGetApp().preset_bundle != nullptr) { + if (const ConfigOptionStrings *opt = GUI::wxGetApp().preset_bundle->project_config.option("filament_colour")) + colors = opt->values; + } + colors.resize(num_physical, "#26A69A"); + return colors; +} + +std::array mix_component_colors_with_filament_mixer(const std::vector> &component_colors, + const std::vector &weights) +{ + if (component_colors.empty() || component_colors.size() != weights.size()) + return { 0.f, 0.f, 0.f }; + + bool has_base = false; + float out_r = 0.f; + float out_g = 0.f; + float out_b = 0.f; + float accumulated = 0.f; + for (size_t idx = 0; idx < component_colors.size(); ++idx) { + const float weight = clamp01(weights[idx]); + if (weight <= k_epsilon) + continue; + + if (!has_base) { + out_r = component_colors[idx][0]; + out_g = component_colors[idx][1]; + out_b = component_colors[idx][2]; + accumulated = weight; + has_base = true; + continue; + } + + const float t = weight / std::max(k_epsilon, accumulated + weight); + float mixed_r = out_r; + float mixed_g = out_g; + float mixed_b = out_b; + filament_mixer_lerp_float(out_r, + out_g, + out_b, + component_colors[idx][0], + component_colors[idx][1], + component_colors[idx][2], + t, + &mixed_r, + &mixed_g, + &mixed_b); + out_r = clamp01(mixed_r); + out_g = clamp01(mixed_g); + out_b = clamp01(mixed_b); + accumulated += weight; + } + + if (!has_base) + return component_colors.front(); + return { out_r, out_g, out_b }; +} + +float color_distance_sq(const std::array &lhs, const std::array &rhs) +{ + const float dr = lhs[0] - rhs[0]; + const float dg = lhs[1] - rhs[1]; + const float db = lhs[2] - rhs[2]; + return dr * dr + dg * dg + db * db; +} + +std::vector best_matching_component_indices_for_semantic_colors(const std::vector> &component_colors, + const std::vector> &semantic_colors) +{ + if (component_colors.empty() || component_colors.size() != semantic_colors.size()) + return {}; + + std::vector permutation(component_colors.size(), 0); + std::iota(permutation.begin(), permutation.end(), size_t(0)); + + std::vector best_permutation = permutation; + float best_error = std::numeric_limits::max(); + do { + float error = 0.f; + for (size_t role_idx = 0; role_idx < semantic_colors.size(); ++role_idx) + error += color_distance_sq(component_colors[permutation[role_idx]], semantic_colors[role_idx]); + + if (error < best_error) { + best_error = error; + best_permutation = permutation; + } + } while (std::next_permutation(permutation.begin(), permutation.end())); + + return best_permutation; +} + +std::vector semantic_component_indices_for_texture_preview(const std::vector> &component_colors, + int filament_color_mode, + bool force_sequential_filaments) +{ + if (force_sequential_filaments) + return {}; + + std::vector> semantic_colors; + switch (filament_color_mode) { + case int(TextureMappingZone::FilamentColorRGB): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMY): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYK): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 0.f, 0.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorCMYW): + semantic_colors = { { { 0.f, 1.f, 1.f } }, { { 1.f, 0.f, 1.f } }, { { 1.f, 1.f, 0.f } }, { { 1.f, 1.f, 1.f } } }; + break; + case int(TextureMappingZone::FilamentColorRGBK): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 0.f, 0.f, 0.f } } }; + break; + case int(TextureMappingZone::FilamentColorRGBW): + semantic_colors = { { { 1.f, 0.f, 0.f } }, { { 0.f, 1.f, 0.f } }, { { 0.f, 0.f, 1.f } }, { { 1.f, 1.f, 1.f } } }; + break; + default: + return {}; + } + + return best_matching_component_indices_for_semantic_colors(component_colors, semantic_colors); +} + +bool texture_preview_uses_generic_solver(const TexturePreviewSimulationSettings &settings) +{ + if (settings.mapping_mode == int(TextureMappingZone::TextureMappingRawValues)) + return false; + + const int clamped_mode = std::clamp(settings.filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + size_t expected_component_count = 0; + switch (clamped_mode) { + case int(TextureMappingZone::FilamentColorRGB): + case int(TextureMappingZone::FilamentColorCMY): + expected_component_count = 3; + break; + case int(TextureMappingZone::FilamentColorCMYK): + case int(TextureMappingZone::FilamentColorCMYW): + case int(TextureMappingZone::FilamentColorRGBK): + case int(TextureMappingZone::FilamentColorRGBW): + expected_component_count = 4; + break; + case int(TextureMappingZone::FilamentColorBW): + expected_component_count = 2; + break; + default: + return true; + } + + return settings.component_colors.size() != expected_component_count; +} + +std::vector build_generic_mix_candidates(const std::vector> &component_colors) +{ + if (component_colors.empty()) + return {}; + + const size_t component_count = component_colors.size(); + const int total_units = component_count <= 4 ? 20 : (component_count <= 6 ? 10 : 6); + std::vector units(component_count, 0); + std::vector candidates; + candidates.reserve(4096); + + std::function recurse = [&](size_t idx, int remaining_units) { + if (idx + 1 == component_count) { + units[idx] = remaining_units; + TexturePreviewMixCandidate candidate; + candidate.weights.assign(component_count, 0.f); + for (size_t weight_idx = 0; weight_idx < component_count; ++weight_idx) + candidate.weights[weight_idx] = float(units[weight_idx]) / float(std::max(1, total_units)); + candidate.rgb = mix_component_colors_with_filament_mixer(component_colors, candidate.weights); + candidates.emplace_back(std::move(candidate)); + return; + } + + for (int unit = 0; unit <= remaining_units; ++unit) { + units[idx] = unit; + recurse(idx + 1, remaining_units - unit); + } + }; + recurse(0, total_units); + return candidates; +} + +std::vector best_component_mix_weights_for_target(const std::vector &candidates, + const std::array &target_rgb) +{ + if (candidates.empty()) + return {}; + + const TexturePreviewMixCandidate *best_candidate = nullptr; + float best_error = std::numeric_limits::max(); + for (const TexturePreviewMixCandidate &candidate : candidates) { + const float error = color_distance_sq(candidate.rgb, target_rgb); + if (error < best_error) { + best_error = error; + best_candidate = &candidate; + } + } + + return best_candidate != nullptr ? best_candidate->weights : std::vector{}; +} + +float apply_texture_tone_gamma(float channel, float tone_gamma) +{ + const float safe_channel = clamp01(channel); + const float safe_gamma = (!std::isfinite(tone_gamma) || tone_gamma <= 0.f) ? 1.f : std::clamp(tone_gamma, 0.5f, 3.f); + if (std::abs(safe_gamma - 1.f) <= 1e-5f) + return safe_channel; + return clamp01(std::pow(safe_channel, 1.f / safe_gamma)); +} + +void apply_texture_contrast_to_mapped_components(std::vector &component_weights, + float contrast_factor, + size_t mapped_component_count) +{ + const size_t count = std::min(mapped_component_count, component_weights.size()); + if (count == 0) + return; + + float mean_weight = 0.f; + for (size_t idx = 0; idx < count; ++idx) + mean_weight += clamp01(component_weights[idx]); + mean_weight /= float(count); + + for (size_t idx = 0; idx < count; ++idx) { + const float safe_weight = clamp01(component_weights[idx]); + component_weights[idx] = clamp01(mean_weight + (safe_weight - mean_weight) * contrast_factor); + } +} + +std::vector optimized_primary_component_weights_for_target(const std::array &target_rgb, + size_t component_count, + int filament_color_mode, + const std::vector> &component_colors, + bool force_sequential_filaments, + const std::vector &semantic_component_indices) +{ + const int clamped_mode = std::clamp(filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + if (clamped_mode == int(TextureMappingZone::FilamentColorAny)) + return {}; + + auto print_visibility_strength = [](float value) { + return clamp01(std::pow(std::max(0.f, value), 0.85f)); + }; + + const float r = clamp01(target_rgb[0]); + const float g = clamp01(target_rgb[1]); + const float b = clamp01(target_rgb[2]); + const float whiteness = std::min({ r, g, b }); + const float darkness = 1.f - std::max({ r, g, b }); + + auto safe_div = [](float numerator, float denominator) { + if (denominator <= k_epsilon) + return 0.f; + return clamp01(numerator / denominator); + }; + const auto component_index_for_role = [&semantic_component_indices](size_t role_idx) { + if (role_idx < semantic_component_indices.size()) + return semantic_component_indices[role_idx]; + return role_idx; + }; + + std::vector weights(component_count, 0.f); + if (clamped_mode == int(TextureMappingZone::FilamentColorRGB)) { + if (component_count != 3) + return {}; + weights[component_index_for_role(0)] = print_visibility_strength(r); + weights[component_index_for_role(1)] = print_visibility_strength(g); + weights[component_index_for_role(2)] = print_visibility_strength(b); + return weights; + } + if (clamped_mode == int(TextureMappingZone::FilamentColorCMY)) { + if (component_count != 3) + return {}; + weights[component_index_for_role(0)] = print_visibility_strength(1.f - r); + weights[component_index_for_role(1)] = print_visibility_strength(1.f - g); + weights[component_index_for_role(2)] = print_visibility_strength(1.f - b); + return weights; + } + if (clamped_mode == int(TextureMappingZone::FilamentColorBW)) { + if (component_count != 2) + return {}; + + const float gray = clamp01(0.2126f * r + 0.7152f * g + 0.0722f * b); + const float black_strength = gray >= 0.5f ? (2.f * (1.f - gray)) : 1.f; + const float white_strength = gray <= 0.5f ? (2.f * gray) : 1.f; + + size_t black_component_idx = 0; + size_t white_component_idx = 1; + if (!force_sequential_filaments && component_colors.size() >= 2) { + const float lum0 = 0.2126f * component_colors[0][0] + 0.7152f * component_colors[0][1] + 0.0722f * component_colors[0][2]; + const float lum1 = 0.2126f * component_colors[1][0] + 0.7152f * component_colors[1][1] + 0.0722f * component_colors[1][2]; + if (lum0 > lum1) { + black_component_idx = 1; + white_component_idx = 0; + } + } + + weights[black_component_idx] = print_visibility_strength(black_strength); + weights[white_component_idx] = print_visibility_strength(white_strength); + return weights; + } + + if (component_count != 4) + return {}; + + if (clamped_mode == int(TextureMappingZone::FilamentColorCMYK)) { + const float k = clamp01(darkness); + const float inv = 1.f - k; + weights[component_index_for_role(0)] = print_visibility_strength(safe_div(1.f - r - k, inv)); + weights[component_index_for_role(1)] = print_visibility_strength(safe_div(1.f - g - k, inv)); + weights[component_index_for_role(2)] = print_visibility_strength(safe_div(1.f - b - k, inv)); + weights[component_index_for_role(3)] = print_visibility_strength(k); + return weights; + } + if (clamped_mode == int(TextureMappingZone::FilamentColorCMYW)) { + const float inv = 1.f - whiteness; + const float r_no_w = safe_div(r - whiteness, inv); + const float g_no_w = safe_div(g - whiteness, inv); + const float b_no_w = safe_div(b - whiteness, inv); + weights[component_index_for_role(0)] = print_visibility_strength((1.f - r_no_w) * inv); + weights[component_index_for_role(1)] = print_visibility_strength((1.f - g_no_w) * inv); + weights[component_index_for_role(2)] = print_visibility_strength((1.f - b_no_w) * inv); + weights[component_index_for_role(3)] = clamp01(std::pow(whiteness, 1.35f)); + return weights; + } + if (clamped_mode == int(TextureMappingZone::FilamentColorRGBK)) { + const float k = clamp01(darkness); + const float inv = 1.f - k; + weights[component_index_for_role(0)] = print_visibility_strength(safe_div(r - k, inv) * inv); + weights[component_index_for_role(1)] = print_visibility_strength(safe_div(g - k, inv) * inv); + weights[component_index_for_role(2)] = print_visibility_strength(safe_div(b - k, inv) * inv); + weights[component_index_for_role(3)] = print_visibility_strength(k); + return weights; + } + if (clamped_mode == int(TextureMappingZone::FilamentColorRGBW)) { + const float inv = 1.f - whiteness; + weights[component_index_for_role(0)] = print_visibility_strength(safe_div(r - whiteness, inv) * inv); + weights[component_index_for_role(1)] = print_visibility_strength(safe_div(g - whiteness, inv) * inv); + weights[component_index_for_role(2)] = print_visibility_strength(safe_div(b - whiteness, inv) * inv); + weights[component_index_for_role(3)] = clamp01(std::pow(whiteness, 1.35f)); + return weights; + } + + return {}; +} + +std::vector component_weights_for_texture_preview(const TexturePreviewSimulationSettings &settings, + const std::array &sample_rgba) +{ + const size_t component_count = settings.component_colors.size(); + if (component_count == 0) + return {}; + + std::array 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); + } + + std::vector desired(component_count, 0.f); + size_t mapped_component_count = component_count; + if (settings.mapping_mode == int(TextureMappingZone::TextureMappingRawValues)) { + const float channels[3] = { target[0], target[1], target[2] }; + const size_t channel_count = std::min(component_count, size_t(3)); + for (size_t channel_idx = 0; channel_idx < channel_count; ++channel_idx) + desired[channel_idx] = clamp01(channels[channel_idx]); + mapped_component_count = channel_count; + } else { + std::vector optimized = optimized_primary_component_weights_for_target(target, + component_count, + settings.filament_color_mode, + settings.component_colors, + settings.force_sequential_filaments, + settings.semantic_component_indices); + if (optimized.size() == component_count) + desired = std::move(optimized); + else { + std::vector best = best_component_mix_weights_for_target(settings.generic_mix_candidates, target); + if (best.size() == component_count) + desired = std::move(best); + } + } + + const float contrast_factor = std::clamp(settings.contrast_pct, 25.f, 300.f) / 100.f; + if (std::abs(contrast_factor - 1.f) > 1e-5f) + apply_texture_contrast_to_mapped_components(desired, contrast_factor, mapped_component_count); + + if (settings.compact_offset_mode) { + float max_weight = 0.f; + for (const float value : desired) + max_weight = std::max(max_weight, clamp01(value)); + if (max_weight > k_epsilon) + for (float &value : desired) + value = clamp01(value / max_weight); + } + + for (size_t idx = 0; idx < desired.size() && idx < settings.component_strength_factors.size(); ++idx) + desired[idx] = clamp01(desired[idx] * settings.component_strength_factors[idx]); + return desired; +} + +void prepare_texture_preview_simulation_settings(TexturePreviewSimulationSettings &settings) +{ + settings.semantic_component_indices = + semantic_component_indices_for_texture_preview(settings.component_colors, + settings.filament_color_mode, + settings.force_sequential_filaments); + if (texture_preview_uses_generic_solver(settings)) + settings.generic_mix_candidates = build_generic_mix_candidates(settings.component_colors); + else + settings.generic_mix_candidates.clear(); +} + +ColorRGBA simulated_texture_preview_color_for_vertex_color(const ColorRGBA *source_color, + const TexturePreviewSimulationSettings *settings) +{ + if (source_color == nullptr) + return { 0.f, 0.f, 0.f, 1.f }; + if (settings == nullptr) + return *source_color; + + const std::array sample_rgba = { + source_color->r(), + source_color->g(), + source_color->b(), + source_color->a() + }; + const std::vector component_weights = component_weights_for_texture_preview(*settings, sample_rgba); + float activity = 0.f; + for (const float weight : component_weights) + activity = std::max(activity, clamp01(weight)); + + if (activity <= k_epsilon) + return *source_color; + + const std::array simulated_rgb = mix_component_colors_with_filament_mixer(settings->component_colors, component_weights); + return { simulated_rgb[0], simulated_rgb[1], simulated_rgb[2], source_color->a() }; +} + +std::optional texture_preview_simulation_settings_for_filament(unsigned int filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const std::vector &physical_colors) +{ + const TextureMappingZone *zone = zone_for_filament(filament_id, num_physical, texture_mgr); + if (zone == nullptr || !is_image_zone(*zone) || !zone->preview_simulate_colors) + return std::nullopt; + + TexturePreviewSimulationSettings settings; + settings.mapping_mode = std::clamp(zone->texture_mapping_mode, + int(TextureMappingZone::TextureMappingFilamentBlending), + int(TextureMappingZone::TextureMappingRawValues)); + settings.filament_color_mode = std::clamp(zone->filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + 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.contrast_pct = std::clamp(zone->contrast_pct, 25.f, 300.f); + settings.tone_gamma = (!std::isfinite(zone->tone_gamma) || zone->tone_gamma <= 0.f) ? + 1.f : + std::clamp(zone->tone_gamma, 0.5f, 3.f); + settings.component_ids = TextureMappingManager::effective_texture_component_ids(*zone, num_physical, physical_colors); + if (settings.component_ids.empty()) + return std::nullopt; + + const bool raw_values_mode = settings.mapping_mode == int(TextureMappingZone::TextureMappingRawValues); + settings.component_colors.reserve(settings.component_ids.size()); + settings.component_strength_factors.reserve(settings.component_ids.size()); + for (const unsigned int component_id : settings.component_ids) { + if (component_id == 0 || size_t(component_id - 1) >= physical_colors.size()) { + if (!raw_values_mode) + return std::nullopt; + settings.component_colors.emplace_back(std::array{ 0.f, 0.f, 0.f }); + } else { + settings.component_colors.emplace_back(decode_color(physical_colors[size_t(component_id - 1)])); + } + + const size_t strength_idx = component_id > 0 ? size_t(component_id - 1) : size_t(0); + const float strength_pct = strength_idx < zone->filament_strengths_pct.size() ? + zone->filament_strengths_pct[strength_idx] : + 100.f; + const float safe_strength_pct = std::isfinite(strength_pct) ? strength_pct : 100.f; + settings.component_strength_factors.emplace_back(std::clamp(safe_strength_pct / 100.f, 0.f, 1.f)); + } + + return settings.component_colors.empty() ? std::nullopt : std::optional(std::move(settings)); +} + +size_t texture_preview_simulation_signature(const ModelVolume &model_volume, + size_t source_signature, + const TexturePreviewSimulationSettings &settings) +{ + size_t signature = source_signature; + auto mix = [&signature](size_t value) { + signature ^= value + 0x9e3779b97f4a7c15ull + (signature << 6) + (signature >> 2); + }; + + mix(reinterpret_cast(&model_volume)); + mix(std::hash{}(settings.mapping_mode)); + mix(std::hash{}(settings.filament_color_mode)); + mix(std::hash{}(settings.force_sequential_filaments ? 1 : 0)); + mix(std::hash{}(settings.limit_texture_resolution ? 1 : 0)); + mix(std::hash{}(settings.compact_offset_mode ? 1 : 0)); + mix(std::hash{}(int(std::lround(settings.contrast_pct * 100.f)))); + mix(std::hash{}(int(std::lround(settings.tone_gamma * 1000.f)))); + for (const unsigned int id : settings.component_ids) + mix(std::hash{}(id)); + for (const auto &color : settings.component_colors) { + mix(std::hash{}(int(std::lround(color[0] * 255.f)))); + mix(std::hash{}(int(std::lround(color[1] * 255.f)))); + mix(std::hash{}(int(std::lround(color[2] * 255.f)))); + } + for (const float strength_factor : settings.component_strength_factors) + mix(std::hash{}(int(std::lround(strength_factor * 1000.f)))); + return signature; +} + +TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t signature, + unsigned int width, + unsigned int height, + std::vector source_rgba, + TexturePreviewSimulationSettings settings) +{ + TexturePreviewSimulationResult result; + result.signature = signature; + if (width == 0 || height == 0 || source_rgba.size() < size_t(width) * size_t(height) * 4) + return result; + + const std::array preview_size = settings.limit_texture_resolution ? + limited_simulated_texture_preview_size(width, height) : + std::array{ width, height }; + result.width = preview_size[0]; + result.height = preview_size[1]; + result.rgba.resize(size_t(result.width) * size_t(result.height) * 4, 0); + if (result.width == 0 || result.height == 0) + return result; + + prepare_texture_preview_simulation_settings(settings); + const bool use_generic_solver = !settings.generic_mix_candidates.empty(); + + std::unordered_map> simulated_color_cache; + simulated_color_cache.reserve(std::min(size_t(result.width) * size_t(result.height), + use_generic_solver ? size_t(32768) : size_t(65536))); + + for (unsigned int y = 0; y < result.height; ++y) { + for (unsigned int x = 0; x < result.width; ++x) { + const std::array source_rgb = + sample_texture_preview_rgb_bilinear(source_rgba, width, height, x, y, result.width, result.height); + const unsigned int cache_key = texture_preview_rgb_cache_key(source_rgb, use_generic_solver); + const size_t idx = (size_t(y) * size_t(result.width) + size_t(x)) * 4; + + auto cached_color = simulated_color_cache.find(cache_key); + if (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]; + result.rgba[idx + 3] = cached_color->second[3]; + continue; + } + + const std::array sample_rgba = { + float(source_rgb[0]) / 255.f, + float(source_rgb[1]) / 255.f, + float(source_rgb[2]) / 255.f, + 1.f + }; + const std::vector component_weights = component_weights_for_texture_preview(settings, sample_rgba); + float activity = 0.f; + for (const float weight : component_weights) + activity = std::max(activity, clamp01(weight)); + + const std::array simulated_rgb = activity > k_epsilon ? + mix_component_colors_with_filament_mixer(settings.component_colors, component_weights) : + std::array{ sample_rgba[0], sample_rgba[1], sample_rgba[2] }; + + const std::array out_rgba = { + to_u8(simulated_rgb[0]), + to_u8(simulated_rgb[1]), + to_u8(simulated_rgb[2]), + 255 + }; + 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]; + } + } + + return result; +} + +std::unordered_map> &texture_preview_simulation_cache() +{ + static auto *cache = new std::unordered_map>(); + return *cache; +} + +std::vector> &abandoned_texture_preview_futures() +{ + static auto *futures = new std::vector>(); + return *futures; +} + +void discard_ready_texture_preview_future(TexturePreviewSimulationCacheEntry &entry) +{ + if (!entry.pending_future.valid() || + entry.pending_future.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; + + try { + (void) entry.pending_future.get(); + } catch (...) { + } + entry.pending_signature = 0; +} + +bool prune_abandoned_texture_preview_futures() +{ + bool pending = false; + auto &futures = abandoned_texture_preview_futures(); + for (auto it = futures.begin(); it != futures.end();) { + if (!it->valid()) { + it = futures.erase(it); + continue; + } + + if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + try { + (void) it->get(); + } catch (...) { + } + it = futures.erase(it); + } else { + pending = true; + ++it; + } + } + return pending; +} + +} // namespace + +void clear_texture_preview_simulation_cache() +{ + prune_abandoned_texture_preview_futures(); + + auto &cache = texture_preview_simulation_cache(); + for (auto it = cache.begin(); it != cache.end();) { + std::shared_ptr &entry = it->second; + if (entry == nullptr) { + it = cache.erase(it); + continue; + } + + if (entry->texture != nullptr) { + entry->texture->reset(); + entry->texture.reset(); + } + entry->uploaded_signature = 0; + entry->pending_signature = 0; + + if (entry->pending_future.valid()) { + if (entry->pending_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + discard_ready_texture_preview_future(*entry); + } else { + abandoned_texture_preview_futures().emplace_back(std::move(entry->pending_future)); + } + } + it = cache.erase(it); + } +} + +namespace { + +bool texture_preview_simulation_is_pending_impl() +{ + const bool abandoned_pending = prune_abandoned_texture_preview_futures(); + auto &cache = texture_preview_simulation_cache(); + for (auto it = cache.begin(); it != cache.end();) { + const std::shared_ptr &entry = it->second; + if (entry == nullptr) { + it = cache.erase(it); + continue; + } + + if (!entry->pending_future.valid()) { + ++it; + continue; + } + + if (entry->pending_future.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return true; + + if (entry->texture == nullptr && entry->pending_signature == 0) { + discard_ready_texture_preview_future(*entry); + it = cache.erase(it); + continue; + } + + ++it; + } + return abandoned_pending; +} + +size_t texture_preview_simulation_cache_key(const ModelVolume &model_volume, unsigned int filament_id) +{ + size_t key = reinterpret_cast(&model_volume); + key ^= std::hash{}(filament_id) + 0x9e3779b97f4a7c15ull + (key << 6) + (key >> 2); + return key; +} + +const GUI::GLTexture *simulated_texture_preview_texture_for_filament(const ModelVolume &model_volume, + unsigned int filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + size_t source_texture_signature, + const GUI::GLTexture &fallback_texture) +{ + const std::vector physical_colors = physical_filament_colors_for_texture_preview(num_physical); + std::optional settings = + texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); + if (!settings.has_value()) + return &fallback_texture; + + const size_t simulation_signature = texture_preview_simulation_signature(model_volume, source_texture_signature, *settings); + auto &cache = texture_preview_simulation_cache(); + const size_t cache_key = texture_preview_simulation_cache_key(model_volume, filament_id); + std::shared_ptr &entry_ref = cache[cache_key]; + if (entry_ref == nullptr) + entry_ref = std::make_shared(); + TexturePreviewSimulationCacheEntry &entry = *entry_ref; + + if (entry.pending_future.valid() && entry.pending_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + TexturePreviewSimulationResult result = entry.pending_future.get(); + if (result.signature == entry.pending_signature && !result.rgba.empty() && result.width > 0 && result.height > 0) { + if (entry.texture == nullptr) + entry.texture = std::make_unique(); + else + entry.texture->reset(); + + if (entry.texture->load_from_raw_data(std::move(result.rgba), result.width, result.height)) { + configure_texture_preview_sampler(*entry.texture); + entry.uploaded_signature = result.signature; + } else { + entry.uploaded_signature = 0; + } + } + entry.pending_signature = 0; + } + + if (entry.texture != nullptr && entry.uploaded_signature == simulation_signature && entry.texture->get_id() != 0) + return entry.texture.get(); + + if (!entry.pending_future.valid()) { + entry.pending_signature = simulation_signature; + const unsigned int width = model_volume.imported_texture_width; + const unsigned int height = model_volume.imported_texture_height; + std::vector source_rgba(model_volume.imported_texture_rgba.begin(), model_volume.imported_texture_rgba.end()); + TexturePreviewSimulationSettings simulation_settings = *settings; + entry.pending_future = std::async(std::launch::async, + [simulation_signature, + width, + height, + source_rgba = std::move(source_rgba), + simulation_settings = std::move(simulation_settings)]() mutable { + return build_simulated_texture_preview_result(simulation_signature, + width, + height, + std::move(source_rgba), + std::move(simulation_settings)); + }); + } + + return &fallback_texture; +} + +bool build_texture_preview_model_for_state(const ModelVolume &model_volume, + const std::vector &state_triangles, + GUI::GLModel &out_model) +{ + if (!model_volume_has_texture_preview_data(model_volume) || state_triangles.empty()) + return false; + + const indexed_triangle_set &its = model_volume.mesh().its; + GUI::GLModel::Geometry geometry; + geometry.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3N3T2 }; + geometry.reserve_vertices(state_triangles.size() * 3); + geometry.reserve_indices(state_triangles.size() * 3); + + unsigned int vertex_index = 0; + for (const TriangleSelector::FacetStateTriangle &triangle : state_triangles) { + if (triangle.source_triangle < 0) + continue; + + const size_t source_triangle = size_t(triangle.source_triangle); + if (source_triangle >= its.indices.size() || + source_triangle >= model_volume.imported_texture_uv_valid.size() || + model_volume.imported_texture_uv_valid[source_triangle] == 0) + continue; + + const size_t uv_offset = source_triangle * 6; + if (uv_offset + 5 >= model_volume.imported_texture_uvs_per_face.size()) + continue; + + const stl_triangle_vertex_indices &source_indices = its.indices[source_triangle]; + if (source_indices[0] < 0 || source_indices[1] < 0 || source_indices[2] < 0) + continue; + if (size_t(source_indices[0]) >= its.vertices.size() || + size_t(source_indices[1]) >= its.vertices.size() || + size_t(source_indices[2]) >= its.vertices.size()) + continue; + + const Vec3f source_p0 = its.vertices[size_t(source_indices[0])].cast(); + const Vec3f source_p1 = its.vertices[size_t(source_indices[1])].cast(); + const Vec3f source_p2 = its.vertices[size_t(source_indices[2])].cast(); + const std::array source_uvs = unwrap_triangle_uvs( + Vec2f(model_volume.imported_texture_uvs_per_face[uv_offset + 0], model_volume.imported_texture_uvs_per_face[uv_offset + 1]), + Vec2f(model_volume.imported_texture_uvs_per_face[uv_offset + 2], model_volume.imported_texture_uvs_per_face[uv_offset + 3]), + Vec2f(model_volume.imported_texture_uvs_per_face[uv_offset + 4], model_volume.imported_texture_uvs_per_face[uv_offset + 5])); + + Vec3f normal = (triangle.vertices[1] - triangle.vertices[0]).cross(triangle.vertices[2] - triangle.vertices[0]); + const float normal_len = normal.norm(); + if (normal_len <= k_epsilon) + continue; + normal /= normal_len; + const Vec3f offset = normal * k_preview_offset; + + std::array leaf_uvs; + bool valid_leaf = true; + for (size_t vertex_idx = 0; vertex_idx < triangle.vertices.size(); ++vertex_idx) { + Vec3f barycentric = Vec3f::Zero(); + if (!barycentric_weights(triangle.vertices[vertex_idx], source_p0, source_p1, source_p2, barycentric)) { + valid_leaf = false; + break; + } + leaf_uvs[vertex_idx] = source_uvs[0] * barycentric.x() + source_uvs[1] * barycentric.y() + source_uvs[2] * barycentric.z(); + } + if (!valid_leaf) + continue; + + for (size_t vertex_idx = 0; vertex_idx < triangle.vertices.size(); ++vertex_idx) + geometry.add_vertex(triangle.vertices[vertex_idx] + offset, normal, leaf_uvs[vertex_idx]); + geometry.add_triangle(vertex_index, vertex_index + 1, vertex_index + 2); + vertex_index += 3; + } + + if (geometry.is_empty()) + return false; + + out_model.init_from(std::move(geometry)); + return true; +} + +bool build_vertex_color_preview_model_for_state(const ModelVolume &model_volume, + const std::vector &state_triangles, + const TexturePreviewSimulationSettings *simulation_settings, + GUI::GLModel &out_model) +{ + if (!model_volume_has_vertex_color_preview_data(model_volume) || state_triangles.empty()) + return false; + + const indexed_triangle_set &its = model_volume.mesh().its; + GUI::GLModel::Geometry geometry; + geometry.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3N3C4 }; + geometry.reserve_vertices(state_triangles.size() * 3); + geometry.reserve_indices(state_triangles.size() * 3); + + std::unordered_map simulated_color_cache; + if (simulation_settings != nullptr) + simulated_color_cache.reserve(std::min(model_volume.imported_vertex_colors_rgba.size(), size_t(65536))); + auto source_vertex_color = [simulation_settings, &simulated_color_cache](uint32_t packed) { + const ColorRGBA source_color = unpack_vertex_color(packed); + if (simulation_settings == nullptr) + return source_color; + + auto cached = simulated_color_cache.find(packed); + if (cached != simulated_color_cache.end()) + return cached->second; + + const ColorRGBA simulated_color = simulated_texture_preview_color_for_vertex_color(&source_color, simulation_settings); + simulated_color_cache.emplace(packed, simulated_color); + return simulated_color; + }; + + unsigned int vertex_index = 0; + for (const TriangleSelector::FacetStateTriangle &triangle : state_triangles) { + if (triangle.source_triangle < 0) + continue; + + const size_t source_triangle = size_t(triangle.source_triangle); + if (source_triangle >= its.indices.size()) + continue; + + const stl_triangle_vertex_indices &source_indices = its.indices[source_triangle]; + if (source_indices[0] < 0 || source_indices[1] < 0 || source_indices[2] < 0) + continue; + if (size_t(source_indices[0]) >= its.vertices.size() || + size_t(source_indices[1]) >= its.vertices.size() || + size_t(source_indices[2]) >= its.vertices.size() || + size_t(source_indices[0]) >= model_volume.imported_vertex_colors_rgba.size() || + size_t(source_indices[1]) >= model_volume.imported_vertex_colors_rgba.size() || + size_t(source_indices[2]) >= model_volume.imported_vertex_colors_rgba.size()) + continue; + + const Vec3f source_p0 = its.vertices[size_t(source_indices[0])].cast(); + const Vec3f source_p1 = its.vertices[size_t(source_indices[1])].cast(); + const Vec3f source_p2 = its.vertices[size_t(source_indices[2])].cast(); + const std::array source_colors = { + source_vertex_color(model_volume.imported_vertex_colors_rgba[size_t(source_indices[0])]), + source_vertex_color(model_volume.imported_vertex_colors_rgba[size_t(source_indices[1])]), + source_vertex_color(model_volume.imported_vertex_colors_rgba[size_t(source_indices[2])]) + }; + + Vec3f normal = (triangle.vertices[1] - triangle.vertices[0]).cross(triangle.vertices[2] - triangle.vertices[0]); + const float normal_len = normal.norm(); + if (normal_len <= k_epsilon) + continue; + normal /= normal_len; + const Vec3f offset = normal * k_preview_offset; + + std::array leaf_colors; + bool valid_leaf = true; + for (size_t vertex_idx = 0; vertex_idx < triangle.vertices.size(); ++vertex_idx) { + Vec3f barycentric = Vec3f::Zero(); + if (!barycentric_weights(triangle.vertices[vertex_idx], source_p0, source_p1, source_p2, barycentric)) { + valid_leaf = false; + break; + } + leaf_colors[vertex_idx] = interpolate_color(source_colors, barycentric); + } + if (!valid_leaf) + continue; + + for (size_t vertex_idx = 0; vertex_idx < triangle.vertices.size(); ++vertex_idx) + geometry.add_vertex(triangle.vertices[vertex_idx] + offset, normal, leaf_colors[vertex_idx]); + geometry.add_triangle(vertex_index, vertex_index + 1, vertex_index + 2); + vertex_index += 3; + } + + if (geometry.is_empty()) + return false; + + out_model.init_from(std::move(geometry)); + return true; +} + +float normalize_angle(float angle) +{ + if (!std::isfinite(angle)) + return 0.f; + float out = std::fmod(angle, 360.f); + if (out < 0.f) + out += 360.f; + return out; +} + +float angular_distance_deg(float a, float b) +{ + const float d = std::abs(normalize_angle(a) - normalize_angle(b)); + return std::min(d, 360.f - d); +} + +float angular_distance_cw(float from_deg, float to_deg) +{ + float d = normalize_angle(to_deg) - normalize_angle(from_deg); + if (d < 0.f) + d += 360.f; + return d; +} + +float component_angular_influence(unsigned int component_id, + float theta_deg, + const std::vector &component_ids, + const std::vector &component_angles_deg) +{ + if (component_ids.empty() || component_ids.size() != component_angles_deg.size()) + return 0.f; + + const auto active_it = std::find(component_ids.begin(), component_ids.end(), component_id); + if (active_it == component_ids.end()) + return 0.f; + if (component_ids.size() == 1) + return 1.f; + + struct SortedComponentAngle { + float angle_deg { 0.f }; + size_t component_idx { 0 }; + }; + + std::vector sorted_angles; + sorted_angles.reserve(component_ids.size()); + for (size_t i = 0; i < component_ids.size(); ++i) + sorted_angles.push_back({ normalize_angle(component_angles_deg[i]), i }); + + std::sort(sorted_angles.begin(), sorted_angles.end(), [](const SortedComponentAngle &lhs, const SortedComponentAngle &rhs) { + return lhs.angle_deg < rhs.angle_deg; + }); + + const size_t active_component_idx = size_t(active_it - component_ids.begin()); + const auto sorted_active_it = std::find_if(sorted_angles.begin(), sorted_angles.end(), [active_component_idx](const SortedComponentAngle &entry) { + return entry.component_idx == active_component_idx; + }); + if (sorted_active_it == sorted_angles.end()) + return 0.f; + + const size_t sorted_pos = size_t(sorted_active_it - sorted_angles.begin()); + const size_t count = sorted_angles.size(); + const float prev_angle = sorted_angles[(sorted_pos + count - 1) % count].angle_deg; + const float self_angle = sorted_angles[sorted_pos].angle_deg; + const float next_angle = sorted_angles[(sorted_pos + 1) % count].angle_deg; + const float prev_to_self_deg = angular_distance_cw(prev_angle, self_angle); + const float self_to_next_deg = angular_distance_cw(self_angle, next_angle); + + if (prev_to_self_deg <= 1e-3f || self_to_next_deg <= 1e-3f) { + float total_weight = 0.f; + float active_weight = 0.f; + for (size_t i = 0; i < component_ids.size(); ++i) { + const float dist = angular_distance_deg(theta_deg, component_angles_deg[i]); + const float weight = std::max(0.f, 1.f - dist / 180.f); + total_weight += weight; + if (component_ids[i] == component_id) + active_weight += weight; + } + + if (total_weight <= k_epsilon) + return 0.f; + return std::clamp(active_weight / total_weight, 0.f, 1.f); + } + + const float theta_norm = normalize_angle(theta_deg); + const float prev_to_theta_deg = angular_distance_cw(prev_angle, theta_norm); + if (prev_to_theta_deg <= prev_to_self_deg + 1e-4f) + return std::clamp(prev_to_theta_deg / prev_to_self_deg, 0.f, 1.f); + + const float self_to_theta_deg = angular_distance_cw(self_angle, theta_norm); + if (self_to_theta_deg <= self_to_next_deg + 1e-4f) + return std::clamp(1.f - self_to_theta_deg / self_to_next_deg, 0.f, 1.f); + + return 0.f; +} + +std::vector decode_surface_gradient_component_ids(const TextureMappingZone &zone, size_t num_physical) +{ + std::vector ids; + bool seen[10] = { false }; + for (const char c : zone.component_ids) { + if (c < '1' || c > '9') + continue; + const unsigned int id = unsigned(c - '0'); + if (id == 0 || id > num_physical || seen[id]) + continue; + seen[id] = true; + ids.emplace_back(id); + } + + auto append_component = [&ids, &seen, num_physical](unsigned int id) { + if (id == 0 || id > num_physical || id > 9 || seen[id]) + return; + seen[id] = true; + ids.emplace_back(id); + }; + + if (ids.size() < 2) { + ids.clear(); + for (bool &flag : seen) + flag = false; + append_component(zone.component_a); + append_component(zone.component_b); + } + + return ids; +} + +float repeated_rotation_progress(float progress01, float repeats, bool reverse_repeats) +{ + const float p = clamp01(progress01); + const float r = std::max(1.f, repeats); + if (r <= 1.f + k_epsilon) + return p; + + float repeated_pos = p * r; + int segment_idx = int(std::floor(repeated_pos)); + float local = repeated_pos - float(segment_idx); + + if (p >= 1.f - k_epsilon) { + segment_idx = std::max(0, int(std::ceil(r)) - 1); + local = 1.f; + } + + if (reverse_repeats && (segment_idx % 2 == 1)) + local = 1.f - local; + return clamp01(local); +} + +float offset_fade_factor(int fade_mode, float progress01) +{ + const float p = clamp01(progress01); + switch (fade_mode) { + case int(TextureMappingZone::OffsetFadeInUp): + return p; + case int(TextureMappingZone::OffsetFadeOutUp): + return 1.f - p; + case int(TextureMappingZone::OffsetFadeInOut): + return 1.f - std::abs(2.f * p - 1.f); + case int(TextureMappingZone::OffsetFadeOutIn): + return std::abs(2.f * p - 1.f); + case int(TextureMappingZone::OffsetFadeOutInReversed): + return 2.f * p - 1.f; + default: + return 1.f; + } +} + +float variable_width_delta(float inset_strength, + float max_width_delta_limit_mm, + float minimum_offset_factor, + float strength_factor) +{ + if (!std::isfinite(max_width_delta_limit_mm) || max_width_delta_limit_mm <= 0.f) + return 0.f; + + const float desired_width_factor = 1.f - std::clamp(inset_strength, 0.f, 1.f); + const float min_width_factor = std::clamp(minimum_offset_factor, 0.f, 1.f); + const float adjusted_width_factor = + min_width_factor + desired_width_factor * std::clamp(strength_factor, 0.f, 1.f) * (1.f - min_width_factor); + + return std::clamp(max_width_delta_limit_mm * (1.f - adjusted_width_factor), 0.f, max_width_delta_limit_mm); +} + +ColorRGBA surface_gradient_preview_color_from_weights(const SurfaceGradientPreviewSettings &settings, + const std::vector &weights) +{ + const std::array rgb = mix_component_colors_with_filament_mixer(settings.component_colors, weights); + return { rgb[0], rgb[1], rgb[2], 1.f }; +} + +ColorRGBA surface_gradient_preview_color_at(const SurfaceGradientPreviewSettings &settings, + const Vec3f &position, + const Vec3f &normal) +{ + if (settings.component_ids.empty() || settings.component_ids.size() != settings.component_colors.size()) + return { 0.15f, 0.65f, 0.6f, 1.f }; + + const float z_span = settings.z_max - settings.z_min; + const float z_progress = z_span > k_epsilon ? + std::clamp((position.z() - settings.z_min) / z_span, 0.f, 1.f) : + 0.f; + + float rotation_deg = 0.f; + if (settings.rotation_enabled) { + const float repeated = repeated_rotation_progress(z_progress, std::max(1.f, settings.repeats), settings.reverse_repeats); + const float direction = settings.clockwise ? -1.f : 1.f; + rotation_deg = direction * 360.f * settings.rotations * repeated; + } + + std::vector rotated_angles = settings.angles_deg; + for (float &angle : rotated_angles) + angle = normalize_angle(angle + rotation_deg); + + Vec2f direction = Vec2f::Zero(); + if (settings.angle_mode == int(TextureMappingZone::OffsetAngleSurfaceNormal)) + direction = Vec2f(normal.x(), normal.y()); + + if (direction.squaredNorm() <= k_epsilon) { + const Vec3f radial = position - settings.center; + direction = Vec2f(radial.x(), radial.y()); + } + if (direction.squaredNorm() <= k_epsilon) + direction = Vec2f(1.f, 0.f); + + const float theta_deg = normalize_angle(float(Geometry::rad2deg(std::atan2(direction.y(), direction.x())))); + + const size_t component_count = settings.component_ids.size(); + std::vector influences(component_count, 0.f); + for (size_t i = 0; i < component_count; ++i) + influences[i] = component_angular_influence(settings.component_ids[i], theta_deg, settings.component_ids, rotated_angles); + + const float fade_factor = std::abs(offset_fade_factor(settings.fade_mode, z_progress)); + std::vector edge_reaches(component_count, 0.f); + for (size_t i = 0; i < component_count; ++i) { + float raw_inset_mm = 0.f; + for (size_t j = 0; j < component_count; ++j) { + if (i == j) + continue; + const float distance_mm = j < settings.distances_mm.size() ? settings.distances_mm[j] : 0.f; + raw_inset_mm += distance_mm * influences[j]; + } + + const float inset_strength = std::clamp(raw_inset_mm / std::max(settings.max_component_distance_mm, k_epsilon), 0.f, 1.f); + const float strength_factor = i < settings.strength_factors.size() ? settings.strength_factors[i] : 1.f; + const float minimum_offset_factor = i < settings.minimum_offset_factors.size() ? settings.minimum_offset_factors[i] : 0.f; + const float width_delta_mm = variable_width_delta(inset_strength * fade_factor, + settings.max_width_delta_limit_mm, + minimum_offset_factor, + strength_factor); + edge_reaches[i] = std::clamp(settings.max_width_delta_limit_mm - width_delta_mm, 0.f, settings.max_width_delta_limit_mm); + } + + const auto minmax_reach = std::minmax_element(edge_reaches.begin(), edge_reaches.end()); + std::vector weights(component_count, 0.f); + if (minmax_reach.first != edge_reaches.end() && (*minmax_reach.second - *minmax_reach.first) > k_epsilon) { + const float base_reach = *minmax_reach.first; + const float reach_span = *minmax_reach.second - base_reach; + for (size_t i = 0; i < component_count; ++i) + weights[i] = std::clamp((edge_reaches[i] - base_reach) / reach_span, 0.f, 1.f); + } else { + std::fill(weights.begin(), weights.end(), 1.f); + } + + return surface_gradient_preview_color_from_weights(settings, weights); +} + +float surface_gradient_preview_config_float(const char *key, float fallback) +{ + if (GUI::wxGetApp().preset_bundle == nullptr) + return fallback; + + const DynamicPrintConfig &config = GUI::wxGetApp().preset_bundle->project_config; + if (const ConfigOptionFloat *opt = config.option(key)) + return std::isfinite(opt->value) ? float(opt->value) : fallback; + return fallback; +} + +std::optional surface_gradient_preview_settings_for_zone(const ModelVolume &model_volume, + const Transform3d &world_matrix, + const TextureMappingZone &zone, + size_t num_physical) +{ + if (!is_gradient_zone(zone)) + return std::nullopt; + + const std::vector colors = physical_filament_colors_for_texture_preview(num_physical); + SurfaceGradientPreviewSettings settings; + settings.component_ids = decode_surface_gradient_component_ids(zone, num_physical); + if (settings.component_ids.size() < 2) + return std::nullopt; + + settings.component_colors.reserve(settings.component_ids.size()); + settings.strength_factors.reserve(settings.component_ids.size()); + settings.minimum_offset_factors.reserve(settings.component_ids.size()); + for (const unsigned int component_id : settings.component_ids) { + if (component_id == 0 || size_t(component_id - 1) >= colors.size()) + return std::nullopt; + settings.component_colors.emplace_back(decode_color(colors[size_t(component_id - 1)])); + + const size_t idx = size_t(component_id - 1); + const float strength_pct = idx < zone.filament_strengths_pct.size() ? zone.filament_strengths_pct[idx] : 100.f; + const float minimum_offset_pct = idx < zone.filament_minimum_offsets_pct.size() ? zone.filament_minimum_offsets_pct[idx] : 0.f; + settings.strength_factors.emplace_back(std::clamp((std::isfinite(strength_pct) ? strength_pct : 100.f) / 100.f, 0.f, 1.f)); + settings.minimum_offset_factors.emplace_back(std::clamp((std::isfinite(minimum_offset_pct) ? minimum_offset_pct : 0.f) / 100.f, 0.f, 1.f)); + } + + const float max_distance_mm = TextureMappingManager::max_component_surface_offset_mm(); + settings.max_component_distance_mm = max_distance_mm; + settings.distances_mm = TextureMappingManager::effective_offset_distances(zone, settings.component_ids.size()); + bool has_nonzero_distance = false; + for (float &distance_mm : settings.distances_mm) { + distance_mm = std::clamp(distance_mm, 0.f, max_distance_mm); + has_nonzero_distance = has_nonzero_distance || distance_mm > k_epsilon; + } + if (!has_nonzero_distance) + return std::nullopt; + + settings.angles_deg = TextureMappingManager::effective_offset_angles(zone, settings.component_ids.size()); + settings.angle_mode = std::clamp(zone.offset_angle_mode, + int(TextureMappingZone::OffsetAngleConfigured), + int(TextureMappingZone::OffsetAngleObjectCenter)); + settings.rotation_enabled = zone.offset_rotation_enabled; + settings.rotations = std::isfinite(zone.offset_rotations) ? zone.offset_rotations : 1.f; + settings.repeats = std::isfinite(zone.offset_repeats) ? std::max(1.f, zone.offset_repeats) : 1.f; + settings.reverse_repeats = zone.offset_reverse_repeats; + settings.clockwise = zone.offset_clockwise; + settings.fade_mode = std::clamp(zone.offset_fade_mode, + int(TextureMappingZone::OffsetFadeNone), + int(TextureMappingZone::OffsetFadeOutInReversed)); + settings.limit_texture_resolution = zone.preview_limit_resolution; + settings.sagging_ratio = std::isfinite(zone.sagging_ratio) ? std::clamp(zone.sagging_ratio, 0.f, 6.f) : 0.f; + + const float base_outer_width_mm = std::max(0.05f, surface_gradient_preview_config_float("texture_mapping_outer_wall_gradient_max_line_width", 0.95f)); + const float min_outer_width_mm = std::clamp(surface_gradient_preview_config_float("texture_mapping_outer_wall_gradient_min_line_width", 0.32f), + 0.05f, + base_outer_width_mm); + const float global_strength_factor = + std::clamp(surface_gradient_preview_config_float("texture_mapping_outer_wall_gradient_global_strength", 100.f) / 100.f, 0.f, 1.f); + settings.max_width_delta_limit_mm = std::min((base_outer_width_mm - min_outer_width_mm) * global_strength_factor, 2.f * max_distance_mm); + if (settings.sagging_ratio > k_epsilon) { + constexpr float preview_layer_height_mm = 0.2f; + settings.max_width_delta_limit_mm = std::min(settings.max_width_delta_limit_mm, preview_layer_height_mm * settings.sagging_ratio); + } + if (!std::isfinite(settings.max_width_delta_limit_mm) || settings.max_width_delta_limit_mm <= k_epsilon) + return std::nullopt; + + const indexed_triangle_set &its = model_volume.mesh().its; + if (its.vertices.empty()) + return std::nullopt; + + Vec3f min_pt(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3f max_pt(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + for (const stl_vertex &vertex : its.vertices) { + const Vec3f p = (world_matrix * vertex.cast()).cast(); + min_pt = min_pt.cwiseMin(p); + max_pt = max_pt.cwiseMax(p); + } + + settings.center = 0.5f * (min_pt + max_pt); + settings.z_min = min_pt.z(); + settings.z_max = max_pt.z(); + return settings; +} + +bool build_surface_gradient_vertex_color_preview_model_for_state(const std::vector &state_triangles, + const SurfaceGradientPreviewSettings &settings, + const Transform3d &world_matrix, + GUI::GLModel &out_model) +{ + if (state_triangles.empty()) + return false; + + GUI::GLModel::Geometry geometry; + geometry.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3N3C4 }; + geometry.reserve_vertices(state_triangles.size() * 3); + geometry.reserve_indices(state_triangles.size() * 3); + + unsigned int vertex_index = 0; + for (const TriangleSelector::FacetStateTriangle &triangle : state_triangles) { + Vec3f normal = (triangle.vertices[1] - triangle.vertices[0]).cross(triangle.vertices[2] - triangle.vertices[0]); + const float normal_len = normal.norm(); + if (normal_len <= k_epsilon) + continue; + normal /= normal_len; + const Vec3f offset = normal * k_preview_offset; + + const Vec3f world_vertices[3] = { + (world_matrix * triangle.vertices[0].cast()).cast(), + (world_matrix * triangle.vertices[1].cast()).cast(), + (world_matrix * triangle.vertices[2].cast()).cast() + }; + Vec3f world_normal = (world_vertices[1] - world_vertices[0]).cross(world_vertices[2] - world_vertices[0]); + const float world_normal_len = world_normal.norm(); + if (world_normal_len <= k_epsilon) + world_normal = normal; + else + world_normal /= world_normal_len; + + const ColorRGBA c0 = surface_gradient_preview_color_at(settings, world_vertices[0], world_normal); + const ColorRGBA c1 = surface_gradient_preview_color_at(settings, world_vertices[1], world_normal); + const ColorRGBA c2 = surface_gradient_preview_color_at(settings, world_vertices[2], world_normal); + + geometry.add_vertex(triangle.vertices[0] + offset, normal, c0); + geometry.add_vertex(triangle.vertices[1] + offset, normal, c1); + geometry.add_vertex(triangle.vertices[2] + offset, normal, c2); + geometry.add_triangle(vertex_index, vertex_index + 1, vertex_index + 2); + vertex_index += 3; + } + + if (geometry.is_empty()) + return false; + + out_model.init_from(std::move(geometry)); + return true; +} + +struct TexturePreviewRenderState +{ + GLboolean blend_enabled { GL_FALSE }; + GLboolean polygon_offset_fill_enabled { GL_FALSE }; + GLboolean depth_mask { GL_TRUE }; + GLfloat polygon_offset_factor { 0.f }; + GLfloat polygon_offset_units { 0.f }; + GLint depth_func { GL_LESS }; +}; + +TexturePreviewRenderState begin_render_state() +{ + TexturePreviewRenderState state; + state.blend_enabled = glIsEnabled(GL_BLEND); + state.polygon_offset_fill_enabled = glIsEnabled(GL_POLYGON_OFFSET_FILL); + glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &state.depth_mask)); + glsafe(::glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &state.polygon_offset_factor)); + glsafe(::glGetFloatv(GL_POLYGON_OFFSET_UNITS, &state.polygon_offset_units)); + glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &state.depth_func)); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + glsafe(::glDepthMask(GL_FALSE)); + glsafe(::glDepthFunc(GL_LEQUAL)); + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(k_polygon_offset_factor, k_polygon_offset_units)); + return state; +} + +void restore_render_state(const TexturePreviewRenderState &state) +{ + glsafe(::glPolygonOffset(state.polygon_offset_factor, state.polygon_offset_units)); + if (!state.polygon_offset_fill_enabled) + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glDepthFunc(state.depth_func)); + glsafe(::glDepthMask(state.depth_mask)); + if (!state.blend_enabled) + glsafe(::glDisable(GL_BLEND)); +} + +void set_common_uniforms(GLShaderProgram &shader, + const Transform3d &model_matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane, + int print_volume_type, + const std::array &print_volume_xy, + const std::array &print_volume_z) +{ + const Transform3d view_model_matrix = view_matrix * model_matrix; + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * + model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("projection_matrix", projection_matrix); + shader.set_uniform("view_normal_matrix", view_normal_matrix); + shader.set_uniform("volume_world_matrix", model_matrix); + shader.set_uniform("z_range", z_range); + shader.set_uniform("clipping_plane", clipping_plane); + shader.set_uniform("print_volume.type", print_volume_type); + shader.set_uniform("print_volume.xy_data", print_volume_xy); + shader.set_uniform("print_volume.z_data", print_volume_z); +} + +} // namespace + +bool texture_preview_simulation_is_pending() +{ + return texture_preview_simulation_is_pending_impl(); +} + +bool build_mmu_texture_preview_models( + const ModelVolume &model_volume, + const std::vector> &triangles_per_type, + const std::vector &state_colors, + unsigned int base_filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + std::vector &out_models, + std::vector &out_colors, + std::vector &out_filament_ids) +{ + out_models.clear(); + out_colors.clear(); + out_filament_ids.clear(); + if (!model_volume_has_texture_preview_data(model_volume)) + return false; + + bool built_any = false; + for (size_t state_id = 0; state_id < triangles_per_type.size(); ++state_id) { + const unsigned int filament_id = filament_id_for_state(state_id, base_filament_id); + const TextureMappingZone *zone = zone_for_filament(filament_id, num_physical, texture_mgr); + if (zone == nullptr || !is_image_zone(*zone)) + continue; + + GUI::GLModel model; + if (!build_texture_preview_model_for_state(model_volume, triangles_per_type[state_id], model)) + continue; + + out_models.emplace_back(std::move(model)); + out_colors.emplace_back(state_id < state_colors.size() ? state_colors[state_id] : + (state_colors.empty() ? ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : state_colors.back())); + out_filament_ids.emplace_back(filament_id); + built_any = true; + } + return built_any; +} + +bool build_mmu_vertex_color_preview_models( + const ModelVolume &model_volume, + const std::vector> &triangles_per_type, + const std::vector &state_colors, + unsigned int base_filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const Transform3d &world_matrix, + std::vector &out_models, + std::vector &out_colors, + std::vector &out_filament_ids) +{ + out_models.clear(); + out_colors.clear(); + out_filament_ids.clear(); + + const std::vector physical_colors = physical_filament_colors_for_texture_preview(num_physical); + bool built_any = false; + for (size_t state_id = 0; state_id < triangles_per_type.size(); ++state_id) { + const unsigned int filament_id = filament_id_for_state(state_id, base_filament_id); + const TextureMappingZone *zone = zone_for_filament(filament_id, num_physical, texture_mgr); + if (zone == nullptr || (!is_image_zone(*zone) && !is_gradient_zone(*zone))) + continue; + + GUI::GLModel model; + if (is_gradient_zone(*zone)) { + std::optional settings = surface_gradient_preview_settings_for_zone(model_volume, world_matrix, *zone, num_physical); + if (!settings) + continue; + if (!build_surface_gradient_vertex_color_preview_model_for_state(triangles_per_type[state_id], *settings, world_matrix, model)) + continue; + } else { + std::optional simulation_settings = + texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); + if (simulation_settings) + prepare_texture_preview_simulation_settings(*simulation_settings); + if (!build_vertex_color_preview_model_for_state(model_volume, + triangles_per_type[state_id], + simulation_settings ? &*simulation_settings : nullptr, + model)) + continue; + } + + out_models.emplace_back(std::move(model)); + out_colors.emplace_back(state_id < state_colors.size() ? state_colors[state_id] : + (state_colors.empty() ? ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : state_colors.back())); + out_filament_ids.emplace_back(filament_id); + built_any = true; + } + return built_any; +} + +bool build_mmu_vertex_color_preview_models( + const ModelVolume &model_volume, + const std::vector> &triangles_per_type, + const std::vector &state_colors, + unsigned int base_filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + std::vector &out_models, + std::vector &out_colors, + std::vector &out_filament_ids) +{ + return build_mmu_vertex_color_preview_models(model_volume, + triangles_per_type, + state_colors, + base_filament_id, + num_physical, + texture_mgr, + Transform3d::Identity(), + out_models, + out_colors, + out_filament_ids); +} + +size_t model_volume_texture_preview_signature(const ModelVolume &model_volume) +{ + size_t signature = 1469598103934665603ull; + auto mix = [&signature](size_t value) { + signature ^= value + 0x9e3779b97f4a7c15ull + (signature << 6) + (signature >> 2); + }; + mix(size_t(model_volume.imported_texture_width)); + mix(size_t(model_volume.imported_texture_height)); + mix(model_volume.imported_texture_rgba.size()); + mix(reinterpret_cast(model_volume.imported_texture_rgba.data())); + mix(model_volume.imported_texture_uvs_per_face.size()); + mix(reinterpret_cast(model_volume.imported_texture_uvs_per_face.data())); + mix(model_volume.imported_texture_uv_valid.size()); + mix(reinterpret_cast(model_volume.imported_texture_uv_valid.data())); + return signature; +} + +bool ensure_model_volume_texture_preview(const ModelVolume &model_volume, + GUI::GLTexture &texture, + size_t &texture_signature) +{ + if (!model_volume_has_texture_preview_data(model_volume)) + return false; + + const size_t preview_signature = model_volume_texture_preview_signature(model_volume); + if (texture.get_id() != 0 && texture_signature == preview_signature) + return true; + + texture.reset(); + std::vector texture_data(model_volume.imported_texture_rgba.begin(), model_volume.imported_texture_rgba.end()); + make_texture_preview_rgba_opaque(texture_data); + if (!texture.load_from_raw_data(std::move(texture_data), model_volume.imported_texture_width, model_volume.imported_texture_height)) { + texture_signature = 0; + return false; + } + + configure_texture_preview_sampler(texture); + texture_signature = preview_signature; + return true; +} + +size_t texture_preview_settings_signature(size_t num_physical, const TextureMappingManager *texture_mgr) +{ + size_t signature = 1469598103934665603ull; + auto signature_mix = [&signature](size_t value) { + signature ^= value + 0x9e3779b97f4a7c15ull + (signature << 6) + (signature >> 2); + }; + auto signature_mix_float = [&signature_mix](float value, float scale = 1000.f) { + const float safe_value = std::isfinite(value) ? value : 0.f; + signature_mix(std::hash{}(int(std::lround(safe_value * scale)))); + }; + + signature_mix(std::hash{}(num_physical)); + if (GUI::wxGetApp().preset_bundle != nullptr) { + if (const ConfigOptionStrings *opt = GUI::wxGetApp().preset_bundle->project_config.option("filament_colour")) + for (const std::string &color : opt->values) + signature_mix(std::hash{}(color)); + } + if (texture_mgr == nullptr) + return signature; + + for (const TextureMappingZone &zone : texture_mgr->zones()) { + signature_mix(std::hash{}(zone.stable_id)); + signature_mix(std::hash{}(zone.zone_id)); + signature_mix(std::hash{}(zone.enabled ? 1 : 0)); + signature_mix(std::hash{}(zone.deleted ? 1 : 0)); + signature_mix(std::hash{}(zone.surface_pattern)); + signature_mix(std::hash{}(zone.component_a)); + signature_mix(std::hash{}(zone.component_b)); + signature_mix(std::hash{}(zone.component_ids)); + signature_mix(std::hash{}(zone.component_weights)); + signature_mix(std::hash{}(zone.offset_distances)); + signature_mix(std::hash{}(zone.offset_angles)); + signature_mix(std::hash{}(zone.offset_mode)); + signature_mix(std::hash{}(zone.offset_rotation_enabled ? 1 : 0)); + signature_mix_float(zone.offset_rotations); + signature_mix_float(zone.offset_repeats); + signature_mix(std::hash{}(zone.offset_reverse_repeats ? 1 : 0)); + signature_mix(std::hash{}(zone.offset_clockwise ? 1 : 0)); + signature_mix(std::hash{}(zone.offset_fade_mode)); + signature_mix(std::hash{}(zone.offset_angle_mode)); + signature_mix(std::hash{}(zone.texture_mapping_mode)); + signature_mix(std::hash{}(zone.filament_color_mode)); + signature_mix(std::hash{}(zone.force_sequential_filaments ? 1 : 0)); + signature_mix(std::hash{}(zone.nonlinear_offset_adjustment ? 1 : 0)); + signature_mix(std::hash{}(zone.compact_offset_mode ? 1 : 0)); + signature_mix(std::hash{}(zone.preview_simulate_colors ? 1 : 0)); + signature_mix(std::hash{}(zone.preview_limit_resolution ? 1 : 0)); + signature_mix_float(zone.sagging_ratio); + signature_mix_float(zone.preview_opacity_pct, 100.f); + signature_mix_float(zone.contrast_pct, 100.f); + signature_mix_float(zone.tone_gamma); + for (const float strength_pct : zone.filament_strengths_pct) + signature_mix_float(strength_pct, 100.f); + for (const float minimum_offset_pct : zone.filament_minimum_offsets_pct) + signature_mix_float(minimum_offset_pct, 100.f); + } + return signature; +} + +void render_model_texture_preview_models( + std::vector &models, + const std::vector &colors, + const std::vector &filament_ids, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const ModelVolume &model_volume, + const GUI::GLTexture &texture, + const Transform3d &model_matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane, + int print_volume_type, + const std::array &print_volume_xy, + const std::array &print_volume_z) +{ + if (models.empty() || colors.size() != models.size() || filament_ids.size() != models.size() || texture.get_id() == 0) + return; + + GLShaderProgram *shader = GUI::wxGetApp().get_shader("painted_texture_preview"); + if (shader == nullptr) + return; + + const TexturePreviewRenderState render_state = begin_render_state(); + shader->start_using(); + set_common_uniforms(*shader, model_matrix, view_matrix, projection_matrix, z_range, clipping_plane, print_volume_type, print_volume_xy, print_volume_z); + glsafe(::glActiveTexture(GL_TEXTURE0)); + shader->set_uniform("uniform_texture", 0); + + const size_t texture_signature = model_volume_texture_preview_signature(model_volume); + GLuint bound_texture_id = 0; + for (size_t idx = 0; idx < models.size(); ++idx) { + const float mix = texture_preview_mix_for_filament(filament_ids[idx], num_physical, texture_mgr); + const bool invalid = texture_preview_settings_invalid_for_filament(filament_ids[idx], num_physical, texture_mgr); + if (mix <= 0.f && !invalid) + continue; + + const GUI::GLTexture *preview_texture = simulated_texture_preview_texture_for_filament(model_volume, + filament_ids[idx], + num_physical, + texture_mgr, + texture_signature, + texture); + if (preview_texture == nullptr || preview_texture->get_id() == 0) + continue; + + if (preview_texture->get_id() != bound_texture_id) { + glsafe(::glBindTexture(GL_TEXTURE_2D, preview_texture->get_id())); + bound_texture_id = preview_texture->get_id(); + } + + shader->set_uniform("texture_preview_mix", mix); + shader->set_uniform("invalid_texture_mapping", invalid); + models[idx].set_color(colors[idx]); + models[idx].render(); + } + + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + shader->stop_using(); + restore_render_state(render_state); +} + +void render_model_vertex_color_preview_models( + std::vector &models, + const std::vector &colors, + const std::vector &filament_ids, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const Transform3d &model_matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane, + int print_volume_type, + const std::array &print_volume_xy, + const std::array &print_volume_z) +{ + if (models.empty() || colors.size() != models.size() || filament_ids.size() != models.size()) + return; + + GLShaderProgram *shader = GUI::wxGetApp().get_shader("painted_vertex_color_preview"); + if (shader == nullptr) + return; + + const TexturePreviewRenderState render_state = begin_render_state(); + shader->start_using(); + set_common_uniforms(*shader, model_matrix, view_matrix, projection_matrix, z_range, clipping_plane, print_volume_type, print_volume_xy, print_volume_z); + + for (size_t idx = 0; idx < models.size(); ++idx) { + const float mix = texture_preview_mix_for_filament(filament_ids[idx], num_physical, texture_mgr); + const bool invalid = texture_preview_settings_invalid_for_filament(filament_ids[idx], num_physical, texture_mgr); + if (mix <= 0.f && !invalid) + continue; + shader->set_uniform("texture_preview_mix", mix); + shader->set_uniform("invalid_texture_mapping", invalid); + models[idx].set_color(colors[idx]); + models[idx].render(); + } + + shader->stop_using(); + restore_render_state(render_state); +} + +} // namespace Slic3r diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.hpp b/src/slic3r/GUI/MMUPaintedTexturePreview.hpp new file mode 100644 index 0000000000..0da4c43741 --- /dev/null +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.hpp @@ -0,0 +1,97 @@ +#ifndef slic3r_MMUPaintedTexturePreview_hpp_ +#define slic3r_MMUPaintedTexturePreview_hpp_ + +#include "GLModel.hpp" +#include "GLTexture.hpp" + +#include "libslic3r/Color.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include +#include + +namespace Slic3r { + +class TextureMappingManager; + +bool build_mmu_texture_preview_models( + const ModelVolume &model_volume, + const std::vector> &triangles_per_type, + const std::vector &state_colors, + unsigned int base_filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + std::vector &out_models, + std::vector &out_colors, + std::vector &out_filament_ids); + +bool build_mmu_vertex_color_preview_models( + const ModelVolume &model_volume, + const std::vector> &triangles_per_type, + const std::vector &state_colors, + unsigned int base_filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const Transform3d &world_matrix, + std::vector &out_models, + std::vector &out_colors, + std::vector &out_filament_ids); + +bool build_mmu_vertex_color_preview_models( + const ModelVolume &model_volume, + const std::vector> &triangles_per_type, + const std::vector &state_colors, + unsigned int base_filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr, + std::vector &out_models, + std::vector &out_colors, + std::vector &out_filament_ids); + +size_t model_volume_texture_preview_signature(const ModelVolume &model_volume); + +bool ensure_model_volume_texture_preview(const ModelVolume &model_volume, + GUI::GLTexture &texture, + size_t &texture_signature); + +bool texture_preview_simulation_is_pending(); +void clear_texture_preview_simulation_cache(); + +size_t texture_preview_settings_signature(size_t num_physical, const TextureMappingManager *texture_mgr); + +void render_model_texture_preview_models( + std::vector &models, + const std::vector &colors, + const std::vector &filament_ids, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const ModelVolume &model_volume, + const GUI::GLTexture &texture, + const Transform3d &model_matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane, + int print_volume_type = -1, + const std::array &print_volume_xy = std::array{ 0.f, 0.f, 0.f, 0.f }, + const std::array &print_volume_z = std::array{ 0.f, 0.f }); + +void render_model_vertex_color_preview_models( + std::vector &models, + const std::vector &colors, + const std::vector &filament_ids, + size_t num_physical, + const TextureMappingManager *texture_mgr, + const Transform3d &model_matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane, + int print_volume_type = -1, + const std::array &print_volume_xy = std::array{ 0.f, 0.f, 0.f, 0.f }, + const std::array &print_volume_z = std::array{ 0.f, 0.f }); + +} // namespace Slic3r + +#endif // slic3r_MMUPaintedTexturePreview_hpp_ diff --git a/src/slic3r/GUI/ObjColorDialog.cpp b/src/slic3r/GUI/ObjColorDialog.cpp index fc5c734148..020faaa0b7 100644 --- a/src/slic3r/GUI/ObjColorDialog.cpp +++ b/src/slic3r/GUI/ObjColorDialog.cpp @@ -396,7 +396,11 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c this->Layout(); } -ObjColorPanel::~ObjColorPanel() { +ObjColorPanel::~ObjColorPanel() +{ + for (ButtonState *item : m_result_icon_list) + delete item; + m_result_icon_list.clear(); } void ObjColorPanel::msw_rescale() diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 8a7d10711a..342abb3162 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1607,9 +1607,17 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const int nums_extruders = 0; if (const ConfigOptionStrings *color_option = dynamic_cast(wxGetApp().preset_bundle->project_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); + size_t total_filaments = size_t(nums_extruders); + if (wxGetApp().preset_bundle != nullptr) { + const std::string serialized = wxGetApp().preset_bundle->project_config.has("texture_mapping_definitions") ? + wxGetApp().preset_bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + wxGetApp().preset_bundle->texture_mapping_zones.load_entries(serialized, color_option->values); + total_filaments = wxGetApp().preset_bundle->texture_mapping_zones.total_filaments(size_t(nums_extruders)); + } if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= nums_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(total_filaments)) plate_extruders.push_back(item.extruder); } } @@ -1729,9 +1737,17 @@ std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D int nums_extruders = 0; if (const ConfigOptionStrings *color_option = dynamic_cast(full_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); + size_t total_filaments = size_t(nums_extruders); + if (wxGetApp().preset_bundle != nullptr) { + const std::string serialized = full_config.has("texture_mapping_definitions") ? + full_config.opt_string("texture_mapping_definitions") : + std::string(); + wxGetApp().preset_bundle->texture_mapping_zones.load_entries(serialized, color_option->values); + total_filaments = wxGetApp().preset_bundle->texture_mapping_zones.total_filaments(size_t(nums_extruders)); + } if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= nums_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(total_filaments)) plate_extruders.push_back(item.extruder); } } @@ -1782,9 +1798,17 @@ std::vector PartPlate::get_extruders_without_support(bool conside_custom_gc int nums_extruders = 0; if (const ConfigOptionStrings* color_option = dynamic_cast(wxGetApp().preset_bundle->project_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); + size_t total_filaments = size_t(nums_extruders); + if (wxGetApp().preset_bundle != nullptr) { + const std::string serialized = wxGetApp().preset_bundle->project_config.has("texture_mapping_definitions") ? + wxGetApp().preset_bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + wxGetApp().preset_bundle->texture_mapping_zones.load_entries(serialized, color_option->values); + total_filaments = wxGetApp().preset_bundle->texture_mapping_zones.total_filaments(size_t(nums_extruders)); + } if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= nums_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(total_filaments)) plate_extruders.push_back(item.extruder); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4f6406d9b4..66d3fcd491 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4,8 +4,12 @@ #include #include +#include +#include #include #include +#include +#include #include #include #include @@ -25,7 +29,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -38,6 +44,9 @@ #include #include #include +#include +#include +#include #ifdef _WIN32 #include #include @@ -65,6 +74,7 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/TextureMapping.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ObjColorUtils.hpp" // For stl export @@ -399,6 +409,802 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove } #endif +static bool model_volume_has_imported_texture_mapping_data(const ModelVolume *volume) +{ + return volume != nullptr && + (!volume->imported_vertex_colors_rgba.empty() || + (!volume->imported_texture_rgba.empty() && + volume->imported_texture_width > 0 && + volume->imported_texture_height > 0)); +} + +static bool model_object_has_imported_texture_mapping_data(const ModelObject *object) +{ + return object != nullptr && std::any_of(object->volumes.begin(), object->volumes.end(), [](const ModelVolume *volume) { + return model_volume_has_imported_texture_mapping_data(volume); + }); +} + +static bool assign_imported_texture_mapping_zone(Model &model) +{ + bool has_imported_data = false; + for (const ModelObject *object : model.objects) { + if (model_object_has_imported_texture_mapping_data(object)) { + has_imported_data = true; + break; + } + } + if (!has_imported_data) + return false; + + PresetBundle *bundle = wxGetApp().preset_bundle; + if (bundle == nullptr) + return false; + + DynamicPrintConfig &project_config = bundle->project_config; + const ConfigOptionStrings *color_opt = project_config.option("filament_colour", false); + if (color_opt == nullptr || color_opt->values.size() < 2) + return false; + + project_config.option("texture_mapping_definitions", true); + bundle->texture_mapping_zones.load_entries(project_config.opt_string("texture_mapping_definitions"), color_opt->values); + const unsigned int zone_id = bundle->texture_mapping_zones.ensure_image_texture_zone(color_opt->values.size(), color_opt->values); + if (zone_id == 0) + return false; + + const std::string serialized = bundle->texture_mapping_zones.serialize_entries(); + if (ConfigOptionString *opt = project_config.option("texture_mapping_definitions")) + opt->value = serialized; + else + project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(serialized)); + DynamicPrintConfig &print_config = bundle->prints.get_edited_preset().config; + if (ConfigOptionString *opt = print_config.option("texture_mapping_definitions")) + opt->value = serialized; + else + print_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(serialized)); + + for (ModelObject *object : model.objects) { + if (!model_object_has_imported_texture_mapping_data(object)) + continue; + object->config.set("extruder", int(zone_id)); + for (ModelVolume *volume : object->volumes) + if (model_volume_has_imported_texture_mapping_data(volume)) + volume->config.set("extruder", int(zone_id)); + } + return true; +} + +static wxColour parse_texture_mapping_color(const std::string &hex) +{ + unsigned char rgba[4] = {38, 166, 154, 255}; + Slic3r::GUI::BitmapCache::parse_color4(hex, rgba); + return wxColour(rgba[0], rgba[1], rgba[2], rgba[3]); +} + +static std::string encode_texture_mapping_component_ids(const std::vector &ids) +{ + std::string out; + bool seen[10] = {false}; + for (const unsigned int id : ids) { + if (id == 0 || id > 9 || seen[id]) + continue; + seen[id] = true; + out.push_back(char('0' + id)); + } + return out; +} + +static std::vector texture_mapping_selected_ids(const TextureMappingZone &zone, size_t num_physical) +{ + std::vector ids = TextureMappingManager::selected_component_ids(zone, num_physical); + ids.erase(std::remove_if(ids.begin(), ids.end(), [num_physical](unsigned int id) { + return id == 0 || id > num_physical || id > 9; + }), ids.end()); + ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + if (ids.size() < 2) { + ids.clear(); + for (size_t i = 1; i <= std::min(num_physical, 9); ++i) + ids.emplace_back(unsigned(i)); + } + if (ids.size() < 2) + ids = {1, 2}; + return ids; +} + +static std::string encode_texture_mapping_float_values(const std::vector &values) +{ + std::ostringstream ss; + for (size_t i = 0; i < values.size(); ++i) { + if (i > 0) + ss << '/'; + ss << std::fixed << std::setprecision(4) << values[i]; + } + return ss.str(); +} + +static wxArrayString texture_mapping_color_mode_choices() +{ + wxArrayString choices; + choices.Add(_L("Generic Solver")); + choices.Add(_L("RGB")); + choices.Add(_L("CMY")); + choices.Add(_L("CMYK")); + choices.Add(_L("CMYW")); + choices.Add(_L("RGBK")); + choices.Add(_L("RGBW")); + choices.Add(_L("BW")); + return choices; +} + +static std::vector texture_mapping_channel_labels(int filament_color_mode) +{ + switch (std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorBW))) { + case int(TextureMappingZone::FilamentColorRGB): + return {_L("Red"), _L("Green"), _L("Blue")}; + case int(TextureMappingZone::FilamentColorCMY): + return {_L("Cyan"), _L("Magenta"), _L("Yellow")}; + case int(TextureMappingZone::FilamentColorCMYK): + return {_L("Cyan"), _L("Magenta"), _L("Yellow"), _L("Black")}; + case int(TextureMappingZone::FilamentColorCMYW): + return {_L("Cyan"), _L("Magenta"), _L("Yellow"), _L("White")}; + case int(TextureMappingZone::FilamentColorRGBK): + return {_L("Red"), _L("Green"), _L("Blue"), _L("Black")}; + case int(TextureMappingZone::FilamentColorRGBW): + return {_L("Red"), _L("Green"), _L("Blue"), _L("White")}; + case int(TextureMappingZone::FilamentColorBW): + return {_L("Black"), _L("White")}; + default: + return {}; + } +} + +static wxString texture_mapping_summary(const TextureMappingZone &zone, size_t num_physical) +{ + wxString summary = zone.is_2d_gradient() ? _L("2D Gradient") : from_u8(TextureMappingManager::filament_color_mode_name(zone.filament_color_mode)); + if (!zone.is_2d_gradient() && summary == "any") + summary = _L("Texture"); + else + summary.MakeUpper(); + + const std::vector ids = texture_mapping_selected_ids(zone, num_physical); + summary += " "; + for (size_t i = 0; i < ids.size(); ++i) { + if (i > 0) + summary += "/"; + summary += wxString::Format("F%u", ids[i]); + } + return summary; +} + +static wxString texture_mapping_menu_label(const TextureMappingZone &zone) +{ + if (zone.is_2d_gradient()) + return _L("Texture Mapping 2D Gradient"); + const std::string color_model = TextureMappingManager::filament_color_mode_name(zone.filament_color_mode); + if (color_model == "any") + return _L("Texture Mapping"); + wxString color_model_text = from_u8(color_model); + color_model_text.MakeUpper(); + return _L("Texture Mapping ") + color_model_text; +} + +static wxSize from_dip_for_parent(wxWindow *parent, const wxSize &size) +{ + return wxWindow::FromDIP(size, parent); +} + +class TextureMappingPatternPreview : public wxPanel +{ +public: + explicit TextureMappingPatternPreview(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, from_dip_for_parent(parent, wxSize(28, 54)), wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(FromDIP(28), FromDIP(54))); + Bind(wxEVT_PAINT, &TextureMappingPatternPreview::on_paint, this); + } + + void set_data(const std::vector &palette, const std::vector &component_ids, const wxColour &fallback) + { + m_palette = palette; + m_component_ids = component_ids; + m_fallback = fallback.IsOk() ? fallback : wxColour("#26A69A"); + Refresh(); + } + +private: + wxColour color_for(unsigned int id) const + { + if (id >= 1 && id <= m_palette.size()) + return m_palette[id - 1]; + return m_fallback; + } + + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + const wxSize size = GetClientSize(); + const int pad = FromDIP(2); + const wxRect rect(pad, pad, std::max(1, size.x - 2 * pad), std::max(1, size.y - 2 * pad)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_fallback)); + dc.DrawRectangle(rect); + if (!m_component_ids.empty()) { + const int slots = int(m_component_ids.size()); + for (int slot = 0; slot < slots; ++slot) { + const int visual_slot = slots - 1 - slot; + const int y0 = rect.GetTop() + int(std::lround(double(visual_slot) * double(rect.GetHeight()) / double(slots))); + const int y1 = rect.GetTop() + int(std::lround(double(visual_slot + 1) * double(rect.GetHeight()) / double(slots))); + dc.SetBrush(wxBrush(color_for(m_component_ids[size_t(slot)]))); + dc.DrawRectangle(rect.GetLeft(), y0, rect.GetWidth(), std::max(1, y1 - y0)); + } + } + dc.SetPen(wxPen(wxGetApp().dark_mode() ? wxColour(118, 118, 118) : wxColour(164, 164, 164), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + } + + std::vector m_palette; + std::vector m_component_ids; + wxColour m_fallback {wxColour("#26A69A")}; +}; + +class TextureMappingNumberSwatch : public wxPanel +{ +public: + explicit TextureMappingNumberSwatch(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, from_dip_for_parent(parent, wxSize(28, 22)), wxBORDER_NONE) + { + SetMinSize(wxSize(FromDIP(28), FromDIP(22))); + SetBackgroundStyle(wxBG_STYLE_PAINT); + Bind(wxEVT_PAINT, &TextureMappingNumberSwatch::on_paint, this); + } + + void set_data(const wxColour &color, unsigned int filament_id) + { + m_color = color.IsOk() ? color : wxColour("#26A69A"); + m_filament_id = filament_id; + Refresh(); + } + +private: + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + const wxSize size = GetClientSize(); + const wxRect rect(0, 0, std::max(1, size.x), std::max(1, size.y)); + dc.SetPen(wxPen(wxGetApp().dark_mode() ? wxColour(118, 118, 118) : wxColour(164, 164, 164), 1)); + dc.SetBrush(wxBrush(m_color)); + wxRect swatch_rect = rect; + swatch_rect.Deflate(1, 1); + dc.DrawRoundedRectangle(swatch_rect, FromDIP(3)); + const wxString label = wxString::Format("%u", m_filament_id); + wxFont font = ::Label::Body_12; + font.SetWeight(wxFONTWEIGHT_BOLD); + dc.SetFont(font); + dc.SetTextForeground(m_color.GetLuminance() < 0.51 ? *wxWHITE : *wxBLACK); + const wxSize text_size = dc.GetTextExtent(label); + dc.DrawText(label, (rect.GetWidth() - text_size.x) / 2, (rect.GetHeight() - text_size.y) / 2); + } + + wxColour m_color {wxColour("#26A69A")}; + unsigned int m_filament_id = 0; +}; + +class TextureMappingOffsetGradientDialog : public wxDialog +{ +public: + TextureMappingOffsetGradientDialog(wxWindow *parent, + const TextureMappingZone &zone, + size_t num_physical, + const std::vector &nozzle_diameters, + const std::vector &palette) + : wxDialog(parent, wxID_ANY, _L("Offset Gradient"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , m_zone(zone) + { + const int gap = FromDIP(8); + const int compact_gap = std::max(FromDIP(3), gap / 2); + m_component_ids = texture_mapping_selected_ids(zone, num_physical); + const float reference_nozzle = float(nozzle_diameters.empty() ? 0.4 : std::max(0.05, nozzle_diameters.front())); + m_max_distance_mm = TextureMappingManager::max_component_surface_offset_mm(reference_nozzle); + const std::vector initial_distances = + TextureMappingManager::effective_offset_distances(zone, m_component_ids.size(), reference_nozzle); + const std::vector initial_angles = + TextureMappingManager::effective_offset_angles(zone, m_component_ids.size()); + float overall_strength_pct = 100.f; + if (!initial_distances.empty()) { + float max_ratio = 0.f; + for (const float distance : initial_distances) + max_ratio = std::max(max_ratio, std::clamp(distance, 0.f, m_max_distance_mm) / std::max(m_max_distance_mm, float(EPSILON))); + overall_strength_pct = std::clamp(max_ratio * 100.f, 0.f, 100.f); + } + + auto *root = new wxBoxSizer(wxVERTICAL); + auto *mode_row = new wxBoxSizer(wxHORIZONTAL); + mode_row->Add(new wxStaticText(this, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + wxArrayString mode_choices; + mode_choices.Add(_L("Basic")); + mode_choices.Add(_L("Advanced")); + m_mode_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, mode_choices); + m_mode_choice->SetSelection(std::clamp(zone.offset_mode, int(TextureMappingZone::OffsetBasic), int(TextureMappingZone::OffsetAdvanced))); + mode_row->Add(m_mode_choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + mode_row->Add(new wxStaticText(this, wxID_ANY, _L("Overall strength")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + m_basic_distance_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 0.0, 100.0, + std::clamp(double(overall_strength_pct), 0.0, 100.0), 1.0); + m_basic_distance_spin->SetDigits(1); + mode_row->Add(m_basic_distance_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + mode_row->Add(new wxStaticText(this, wxID_ANY, _L("%")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + m_basic_angle_label = new wxStaticText(this, wxID_ANY, _L("Offset angle")); + mode_row->Add(m_basic_angle_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + m_basic_angle_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 0.0, 360.0, + initial_angles.empty() ? 0.0 : std::clamp(double(initial_angles.front()), 0.0, 360.0), 1.0); + m_basic_angle_spin->SetDigits(1); + mode_row->Add(m_basic_angle_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + m_basic_angle_units = new wxStaticText(this, wxID_ANY, _L("deg")); + mode_row->Add(m_basic_angle_units, 0, wxALIGN_CENTER_VERTICAL); + root->Add(mode_row, 0, wxEXPAND | wxALL, gap); + + auto *rotation_box = new wxStaticBoxSizer(wxVERTICAL, this, _L("Rotation over model height")); + m_rotation_enabled = new wxCheckBox(this, wxID_ANY, _L("Enable rotation")); + m_rotation_enabled->SetValue(zone.offset_rotation_enabled); + rotation_box->Add(m_rotation_enabled, 0, wxBOTTOM, compact_gap); + auto *rotation_row = new wxBoxSizer(wxHORIZONTAL); + rotation_row->Add(new wxStaticText(this, wxID_ANY, _L("Rotation count")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + m_rotations_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(80), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, -64.0, 64.0, + std::clamp(double(zone.offset_rotations), -64.0, 64.0), 0.1); + m_rotations_spin->SetDigits(2); + rotation_row->Add(m_rotations_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + rotation_row->Add(new wxStaticText(this, wxID_ANY, _L("Repeats")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + m_repeats_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(80), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 1.0, 64.0, + std::max(1.0, double(zone.offset_repeats)), 0.1); + m_repeats_spin->SetDigits(2); + rotation_row->Add(m_repeats_spin, 0, wxALIGN_CENTER_VERTICAL); + rotation_box->Add(rotation_row, 0, wxEXPAND | wxBOTTOM, compact_gap); + auto *rotation_flags_row = new wxBoxSizer(wxHORIZONTAL); + m_reverse_repeats = new wxCheckBox(this, wxID_ANY, _L("Reverse repeats")); + m_reverse_repeats->SetValue(zone.offset_reverse_repeats); + rotation_flags_row->Add(m_reverse_repeats, 0, wxRIGHT, gap); + m_clockwise = new wxCheckBox(this, wxID_ANY, _L("Clockwise")); + m_clockwise->SetValue(zone.offset_clockwise); + rotation_flags_row->Add(m_clockwise, 0); + rotation_box->Add(rotation_flags_row, 0, wxEXPAND); + root->Add(rotation_box, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *fade_row = new wxBoxSizer(wxHORIZONTAL); + fade_row->Add(new wxStaticText(this, wxID_ANY, _L("Fade")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + wxArrayString fade_choices; + fade_choices.Add(_L("None")); + fade_choices.Add(_L("Fade in (going up)")); + fade_choices.Add(_L("Fade out (going up)")); + fade_choices.Add(_L("Fade in and out")); + fade_choices.Add(_L("Fade out and in")); + fade_choices.Add(_L("Fade out and in (mirrored)")); + m_fade_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, fade_choices); + m_fade_choice->SetSelection(std::clamp(zone.offset_fade_mode, int(TextureMappingZone::OffsetFadeNone), int(TextureMappingZone::OffsetFadeOutInReversed))); + fade_row->Add(m_fade_choice, 1, wxALIGN_CENTER_VERTICAL); + root->Add(fade_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *angle_mode_row = new wxBoxSizer(wxHORIZONTAL); + angle_mode_row->Add(new wxStaticText(this, wxID_ANY, _L("Gradient calculated from")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + wxArrayString angle_mode_choices; + angle_mode_choices.Add(_L("Surface normal")); + angle_mode_choices.Add(_L("Angle from object center")); + m_angle_mode_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, angle_mode_choices); + m_angle_mode_choice->SetSelection(zone.offset_angle_mode == int(TextureMappingZone::OffsetAngleSurfaceNormal) ? 0 : 1); + angle_mode_row->Add(m_angle_mode_choice, 1, wxALIGN_CENTER_VERTICAL); + root->Add(angle_mode_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *components_box = new wxStaticBoxSizer(wxVERTICAL, this, _L("Per-color strengths")); + const float overall_factor = std::clamp(overall_strength_pct / 100.f, 0.f, 1.f); + for (size_t i = 0; i < m_component_ids.size(); ++i) { + auto *row = new wxBoxSizer(wxHORIZONTAL); + wxStaticText *label = new wxStaticText(this, wxID_ANY, wxString::Format("F%d", int(m_component_ids[i]))); + if (m_component_ids[i] >= 1 && m_component_ids[i] <= palette.size()) + label->SetForegroundColour(palette[m_component_ids[i] - 1]); + row->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + float initial_strength = 100.f; + if (i < initial_distances.size() && overall_factor > EPSILON && m_max_distance_mm > EPSILON) + initial_strength = std::clamp(100.f * initial_distances[i] / (m_max_distance_mm * overall_factor), 0.f, 100.f); + wxSpinCtrlDouble *distance_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 0.0, 100.0, + std::clamp(double(initial_strength), 0.0, 100.0), 1.0); + distance_spin->SetDigits(1); + row->Add(distance_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + row->Add(new wxStaticText(this, wxID_ANY, _L("%")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + wxSpinCtrlDouble *angle_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, 0.0, 360.0, + i < initial_angles.size() ? std::clamp(double(initial_angles[i]), 0.0, 360.0) : 0.0, 1.0); + angle_spin->SetDigits(1); + row->Add(angle_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + row->Add(new wxStaticText(this, wxID_ANY, _L("deg")), 0, wxALIGN_CENTER_VERTICAL); + components_box->Add(row, 0, wxEXPAND | wxTOP, i == 0 ? 0 : compact_gap); + m_distance_spins.emplace_back(distance_spin); + m_angle_spins.emplace_back(angle_spin); + m_component_rows.emplace_back(row); + } + root->Add(components_box, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *buttons_row = new wxBoxSizer(wxHORIZONTAL); + auto *remove_btn = new wxButton(this, wxID_ANY, _L("Remove")); + buttons_row->Add(remove_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + buttons_row->AddStretchSpacer(1); + buttons_row->Add(new wxButton(this, wxID_CANCEL), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); + buttons_row->Add(new wxButton(this, wxID_OK), 0, wxALIGN_CENTER_VERTICAL); + root->Add(buttons_row, 0, wxEXPAND | wxALL, gap); + remove_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { + m_remove_requested = true; + EndModal(wxID_OK); + }); + + m_mode_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent &) { update_advanced_visibility(); }); + SetSizerAndFit(root); + SetMinSize(wxSize(FromDIP(560), std::max(GetSize().GetHeight(), FromDIP(420)))); + update_advanced_visibility(); + } + + bool apply_to(TextureMappingZone &out) + { + out = m_zone; + if (m_remove_requested) { + out.reset_offset_settings(); + return true; + } + out.offset_mode = std::clamp(m_mode_choice ? m_mode_choice->GetSelection() : int(TextureMappingZone::OffsetBasic), + int(TextureMappingZone::OffsetBasic), int(TextureMappingZone::OffsetAdvanced)); + out.offset_rotation_enabled = m_rotation_enabled && m_rotation_enabled->GetValue(); + out.offset_rotations = m_rotations_spin ? float(m_rotations_spin->GetValue()) : 1.f; + out.offset_repeats = m_repeats_spin ? std::max(1.f, float(m_repeats_spin->GetValue())) : 1.f; + out.offset_reverse_repeats = m_reverse_repeats && m_reverse_repeats->GetValue(); + out.offset_clockwise = m_clockwise && m_clockwise->GetValue(); + out.offset_fade_mode = m_fade_choice ? std::clamp(m_fade_choice->GetSelection(), int(TextureMappingZone::OffsetFadeNone), int(TextureMappingZone::OffsetFadeOutInReversed)) : + int(TextureMappingZone::OffsetFadeNone); + out.offset_angle_mode = (m_angle_mode_choice && m_angle_mode_choice->GetSelection() == 0) ? + int(TextureMappingZone::OffsetAngleSurfaceNormal) : int(TextureMappingZone::OffsetAngleObjectCenter); + const float overall_factor = std::clamp(float(m_basic_distance_spin ? m_basic_distance_spin->GetValue() : 100.0) / 100.f, 0.f, 1.f); + float basic_angle = m_basic_angle_spin ? float(m_basic_angle_spin->GetValue()) : 0.f; + basic_angle = std::fmod(basic_angle, 360.f); + if (basic_angle < 0.f) + basic_angle += 360.f; + std::vector distances(m_component_ids.size(), 0.f); + std::vector angles(m_component_ids.size(), 0.f); + for (size_t i = 0; i < m_component_ids.size(); ++i) { + float component_strength = 100.f; + if (out.offset_mode == int(TextureMappingZone::OffsetAdvanced) && i < m_distance_spins.size() && m_distance_spins[i]) + component_strength = float(m_distance_spins[i]->GetValue()); + distances[i] = std::clamp(m_max_distance_mm * overall_factor * std::clamp(component_strength / 100.f, 0.f, 1.f), 0.f, m_max_distance_mm); + if (out.offset_mode == int(TextureMappingZone::OffsetAdvanced) && i < m_angle_spins.size() && m_angle_spins[i]) + angles[i] = float(m_angle_spins[i]->GetValue()); + else + angles[i] = basic_angle + (360.f * float(i)) / std::max(1.f, float(m_component_ids.size())); + angles[i] = std::fmod(angles[i], 360.f); + if (angles[i] < 0.f) + angles[i] += 360.f; + } + out.component_ids = encode_texture_mapping_component_ids(m_component_ids); + out.component_a = m_component_ids.empty() ? 1 : m_component_ids.front(); + out.component_b = m_component_ids.size() > 1 ? m_component_ids[1] : out.component_a; + out.offset_distances = encode_texture_mapping_float_values(distances); + out.offset_angles = encode_texture_mapping_float_values(angles); + return true; + } + +private: + void update_advanced_visibility() + { + const bool show_advanced = m_mode_choice && m_mode_choice->GetSelection() == int(TextureMappingZone::OffsetAdvanced); + if (m_basic_angle_label) + m_basic_angle_label->Show(!show_advanced); + if (m_basic_angle_spin) + m_basic_angle_spin->Show(!show_advanced); + if (m_basic_angle_units) + m_basic_angle_units->Show(!show_advanced); + for (wxSizer *row : m_component_rows) + if (row) + row->ShowItems(show_advanced); + Layout(); + Fit(); + } + + TextureMappingZone m_zone; + std::vector m_component_ids; + wxChoice *m_mode_choice {nullptr}; + wxSpinCtrlDouble *m_basic_distance_spin {nullptr}; + wxStaticText *m_basic_angle_label {nullptr}; + wxSpinCtrlDouble *m_basic_angle_spin {nullptr}; + wxStaticText *m_basic_angle_units {nullptr}; + wxCheckBox *m_rotation_enabled {nullptr}; + wxSpinCtrlDouble *m_rotations_spin {nullptr}; + wxSpinCtrlDouble *m_repeats_spin {nullptr}; + wxCheckBox *m_reverse_repeats {nullptr}; + wxCheckBox *m_clockwise {nullptr}; + wxChoice *m_fade_choice {nullptr}; + wxChoice *m_angle_mode_choice {nullptr}; + std::vector m_distance_spins; + std::vector m_angle_spins; + std::vector m_component_rows; + float m_max_distance_mm {0.2f}; + bool m_remove_requested {false}; +}; + +class TextureMappingAdvancedOptionsDialog : public wxDialog +{ +public: + TextureMappingAdvancedOptionsDialog(wxWindow *parent, + int texture_mapping_mode, + int filament_color_mode, + const std::vector &component_ids, + const std::vector &component_strengths_pct, + const std::vector &component_minimum_offsets_pct, + float tone_gamma, + float sagging_ratio, + float preview_opacity_pct, + bool force_sequential_filaments, + bool auto_adjust_filament_selection, + bool preview_limit_resolution, + bool reduce_outer_surface_texture, + bool seam_hiding, + bool nonlinear_offset_adjustment, + bool compact_offset_mode, + int initial_options_tab) + : wxDialog(parent, wxID_ANY, _L("Texture Mapping Options"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + { + const int gap = FromDIP(8); + auto *root = new wxBoxSizer(wxVERTICAL); + auto *tab_row = new wxBoxSizer(wxHORIZONTAL); + tab_row->Add(new wxStaticText(this, wxID_ANY, _L("Options")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + wxArrayString tab_choices; + tab_choices.Add(_L("Image Options")); + tab_choices.Add(_L("Filament Calibration")); + tab_choices.Add(_L("Preview Options")); + tab_choices.Add(_L("Experimental Options")); + m_options_tab_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, tab_choices); + m_options_tab_choice->SetSelection(std::clamp(initial_options_tab, 0, 3)); + tab_row->Add(m_options_tab_choice, 1, wxALIGN_CENTER_VERTICAL); + root->Add(tab_row, 0, wxEXPAND | wxALL, gap); + + m_options_book = new wxSimplebook(this, wxID_ANY); + auto *image_page = new wxPanel(m_options_book, wxID_ANY); + auto *image_root = new wxBoxSizer(wxVERTICAL); + image_page->SetSizer(image_root); + auto *filament_page = new wxPanel(m_options_book, wxID_ANY); + auto *filament_root = new wxBoxSizer(wxVERTICAL); + filament_page->SetSizer(filament_root); + auto *preview_page = new wxPanel(m_options_book, wxID_ANY); + auto *preview_root = new wxBoxSizer(wxVERTICAL); + preview_page->SetSizer(preview_root); + auto *experimental_page = new wxPanel(m_options_book, wxID_ANY); + auto *experimental_root = new wxBoxSizer(wxVERTICAL); + experimental_page->SetSizer(experimental_root); + + auto *mapping_row = new wxBoxSizer(wxHORIZONTAL); + mapping_row->Add(new wxStaticText(image_page, wxID_ANY, _L("Interpret texture color as")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + wxArrayString mapping_choices; + mapping_choices.Add(_L("Target color")); + mapping_choices.Add(_L("Raw filament offset")); + m_texture_mapping_mode_choice = new wxChoice(image_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, mapping_choices); + m_texture_mapping_mode_choice->SetSelection(std::clamp(texture_mapping_mode, int(TextureMappingZone::TextureMappingFilamentBlending), int(TextureMappingZone::TextureMappingRawValues))); + mapping_row->Add(m_texture_mapping_mode_choice, 1, wxALIGN_CENTER_VERTICAL); + image_root->Add(mapping_row, 0, wxEXPAND | wxALL, gap); + + auto *tone_gamma_row = new wxBoxSizer(wxHORIZONTAL); + tone_gamma_row->Add(new wxStaticText(image_page, wxID_ANY, _L("Tone gamma")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + m_tone_gamma_spin = new wxSpinCtrlDouble(image_page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT, 0.5, 3.0, std::clamp(double(tone_gamma), 0.5, 3.0), 0.05); + m_tone_gamma_spin->SetDigits(2); + tone_gamma_row->Add(m_tone_gamma_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + tone_gamma_row->Add(new wxStaticText(image_page, wxID_ANY, _L("x")), 0, wxALIGN_CENTER_VERTICAL); + image_root->Add(tone_gamma_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *sagging_row = new wxBoxSizer(wxHORIZONTAL); + sagging_row->Add(new wxStaticText(filament_page, wxID_ANY, _L("Sagging ratio limit")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + m_sagging_ratio_spin = new wxSpinCtrlDouble(filament_page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(84), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT, 0.0, 6.0, std::clamp(double(sagging_ratio), 0.0, 6.0), 0.1); + m_sagging_ratio_spin->SetDigits(2); + sagging_row->Add(m_sagging_ratio_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + sagging_row->Add(new wxStaticText(filament_page, wxID_ANY, _L("x h")), 0, wxALIGN_CENTER_VERTICAL); + filament_root->Add(sagging_row, 0, wxEXPAND | wxALL, gap); + + m_force_sequential_filaments_checkbox = new wxCheckBox(filament_page, wxID_ANY, _L("Force sequential order for filaments")); + m_force_sequential_filaments_checkbox->SetValue(force_sequential_filaments); + filament_root->Add(m_force_sequential_filaments_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *minimum_offsets_box = new wxStaticBoxSizer(wxVERTICAL, filament_page, _L("Per-filament minimum offset")); + auto *strengths_box = new wxStaticBoxSizer(wxVERTICAL, filament_page, _L("Per-filament strength")); + const std::vector channel_labels = texture_mapping_channel_labels(filament_color_mode); + auto component_label = [&component_ids, &channel_labels](size_t i) { + wxString text = wxString::Format("F%d", int(component_ids[i])); + if (i < channel_labels.size() && !channel_labels[i].empty()) + text += wxString::Format(" (%s)", channel_labels[i]); + return text; + }; + auto add_percent_row = [this, gap, filament_page, component_label](wxStaticBoxSizer *box, + size_t idx, + int value, + std::vector &sliders, + std::vector &spins) { + auto *row = new wxBoxSizer(wxHORIZONTAL); + row->Add(new wxStaticText(filament_page, wxID_ANY, component_label(idx)), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *slider = new wxSlider(filament_page, wxID_ANY, value, 0, 100, wxDefaultPosition, wxSize(FromDIP(180), -1), wxSL_HORIZONTAL | wxSL_AUTOTICKS); + auto *spin = new wxSpinCtrl(filament_page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(70), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT, 0, 100, value); + slider->Bind(wxEVT_SLIDER, [spin](wxCommandEvent &evt) { + if (spin) + spin->SetValue(evt.GetInt()); + }); + spin->Bind(wxEVT_SPINCTRL, [slider](wxSpinEvent &evt) { + if (slider) + slider->SetValue(evt.GetInt()); + }); + row->Add(slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + row->Add(spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + row->Add(new wxStaticText(filament_page, wxID_ANY, _L("%")), 0, wxALIGN_CENTER_VERTICAL); + sliders.emplace_back(slider); + spins.emplace_back(spin); + box->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + }; + for (size_t i = 0; i < component_ids.size(); ++i) { + const int offset_value = i < component_minimum_offsets_pct.size() ? std::clamp(int(std::lround(component_minimum_offsets_pct[i])), 0, 100) : 0; + add_percent_row(minimum_offsets_box, i, offset_value, m_minimum_offset_sliders, m_minimum_offset_spins); + } + for (size_t i = 0; i < component_ids.size(); ++i) { + const int strength_value = i < component_strengths_pct.size() ? std::clamp(int(std::lround(component_strengths_pct[i])), 0, 100) : 100; + add_percent_row(strengths_box, i, strength_value, m_strength_sliders, m_strength_spins); + } + filament_root->Add(minimum_offsets_box, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + filament_root->Add(strengths_box, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + auto *reset_btn = new wxButton(filament_page, wxID_ANY, _L("Reset strengths and offsets")); + reset_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { reset_strengths_and_offsets(); }); + filament_root->Add(reset_btn, 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + auto *preview_box = new wxStaticBoxSizer(wxVERTICAL, preview_page, _L("3D Preview")); + auto *preview_opacity_row = new wxBoxSizer(wxHORIZONTAL); + preview_opacity_row->Add(new wxStaticText(preview_page, wxID_ANY, _L("Texture opacity")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + const int opacity = std::clamp(int(std::lround(preview_opacity_pct)), 0, 100); + m_preview_opacity_slider = new wxSlider(preview_page, wxID_ANY, opacity, 0, 100, wxDefaultPosition, wxSize(FromDIP(180), -1), wxSL_HORIZONTAL | wxSL_AUTOTICKS); + m_preview_opacity_spin = new wxSpinCtrl(preview_page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(70), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT, 0, 100, opacity); + m_preview_opacity_slider->Bind(wxEVT_SLIDER, [this](wxCommandEvent &evt) { + if (m_preview_opacity_spin) + m_preview_opacity_spin->SetValue(evt.GetInt()); + }); + m_preview_opacity_spin->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent &evt) { + if (m_preview_opacity_slider) + m_preview_opacity_slider->SetValue(evt.GetInt()); + }); + preview_opacity_row->Add(m_preview_opacity_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + preview_opacity_row->Add(m_preview_opacity_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + preview_opacity_row->Add(new wxStaticText(preview_page, wxID_ANY, _L("%")), 0, wxALIGN_CENTER_VERTICAL); + preview_box->Add(preview_opacity_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_auto_adjust_filament_selection_checkbox = new wxCheckBox(preview_page, wxID_ANY, _L("Auto adjust filament selection when changing color mode")); + m_auto_adjust_filament_selection_checkbox->SetValue(auto_adjust_filament_selection); + preview_box->Add(m_auto_adjust_filament_selection_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_preview_limit_resolution_checkbox = new wxCheckBox(preview_page, wxID_ANY, _L("Limit color simulation texture resolution")); + m_preview_limit_resolution_checkbox->SetValue(preview_limit_resolution); + preview_box->Add(m_preview_limit_resolution_checkbox, 0, wxEXPAND | wxALL, gap); + preview_root->Add(preview_box, 0, wxEXPAND | wxALL, gap); + + auto *experimental_box = new wxStaticBoxSizer(wxVERTICAL, experimental_page, _L("Surface Texture")); + m_reduce_outer_surface_texture_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Reduce outer surface texture")); + m_reduce_outer_surface_texture_checkbox->SetValue(reduce_outer_surface_texture); + experimental_box->Add(m_reduce_outer_surface_texture_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_seam_hiding_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Seam Hiding")); + m_seam_hiding_checkbox->SetValue(seam_hiding); + experimental_box->Add(m_seam_hiding_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + m_nonlinear_offset_adjustment_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Non-linear offset adjustment")); + m_nonlinear_offset_adjustment_checkbox->SetValue(nonlinear_offset_adjustment); + m_nonlinear_offset_adjustment_checkbox->SetToolTip( + _L("Adjusts line-width offsets using a surface-visibility model derived from Kuipers et al. 2018.")); + experimental_box->Add(m_nonlinear_offset_adjustment_checkbox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + m_compact_offset_mode_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Compact Offset Mode")); + m_compact_offset_mode_checkbox->SetValue(compact_offset_mode); + m_compact_offset_mode_checkbox->SetToolTip( + _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); + experimental_root->Add(experimental_box, 0, wxEXPAND | wxALL, gap); + + m_options_book->AddPage(image_page, _L("Image Options"), true); + m_options_book->AddPage(filament_page, _L("Filament Calibration")); + m_options_book->AddPage(preview_page, _L("Preview Options")); + m_options_book->AddPage(experimental_page, _L("Experimental Options")); + m_options_book->SetMinSize(wxSize(FromDIP(420), std::max({image_page->GetBestSize().y, filament_page->GetBestSize().y, preview_page->GetBestSize().y, experimental_page->GetBestSize().y}))); + m_options_tab_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent &evt) { + if (m_options_book) + m_options_book->SetSelection(std::clamp(evt.GetSelection(), 0, 3)); + }); + if (m_options_book) + m_options_book->SetSelection(std::clamp(initial_options_tab, 0, 3)); + root->Add(m_options_book, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + if (wxSizer *buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) + root->Add(buttons, 0, wxEXPAND | wxALL, gap); + SetSizerAndFit(root); + SetMinSize(wxSize(FromDIP(420), GetBestSize().GetHeight())); + CentreOnParent(); + } + + int texture_mapping_mode() const + { + return m_texture_mapping_mode_choice ? + std::clamp(m_texture_mapping_mode_choice->GetSelection(), + int(TextureMappingZone::TextureMappingFilamentBlending), + int(TextureMappingZone::TextureMappingRawValues)) : + int(TextureMappingZone::TextureMappingFilamentBlending); + } + + float tone_gamma() const { return float(std::clamp(m_tone_gamma_spin ? m_tone_gamma_spin->GetValue() : 1.0, 0.5, 3.0)); } + float sagging_ratio() const { return float(std::clamp(m_sagging_ratio_spin ? m_sagging_ratio_spin->GetValue() : 0.0, 0.0, 6.0)); } + float preview_opacity_pct() const { return float(std::clamp(m_preview_opacity_spin ? m_preview_opacity_spin->GetValue() : 100, 0, 100)); } + bool force_sequential_filaments() const { return m_force_sequential_filaments_checkbox && m_force_sequential_filaments_checkbox->GetValue(); } + bool auto_adjust_filament_selection() const { return m_auto_adjust_filament_selection_checkbox == nullptr || m_auto_adjust_filament_selection_checkbox->GetValue(); } + bool preview_limit_resolution() const { return m_preview_limit_resolution_checkbox == nullptr || m_preview_limit_resolution_checkbox->GetValue(); } + 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(); } + int selected_options_tab() const { return std::clamp(m_options_tab_choice ? m_options_tab_choice->GetSelection() : 0, 0, 3); } + + std::vector component_strengths_pct() const + { + std::vector out; + out.reserve(m_strength_spins.size()); + for (wxSpinCtrl *spin : m_strength_spins) + out.emplace_back(float(std::clamp(spin ? spin->GetValue() : 100, 0, 100))); + return out; + } + + std::vector component_minimum_offsets_pct() const + { + std::vector out; + out.reserve(m_minimum_offset_spins.size()); + for (wxSpinCtrl *spin : m_minimum_offset_spins) + out.emplace_back(float(std::clamp(spin ? spin->GetValue() : 0, 0, 100))); + return out; + } + +private: + void reset_strengths_and_offsets() + { + for (wxSlider *slider : m_minimum_offset_sliders) + if (slider) + slider->SetValue(0); + for (wxSpinCtrl *spin : m_minimum_offset_spins) + if (spin) + spin->SetValue(0); + for (wxSlider *slider : m_strength_sliders) + if (slider) + slider->SetValue(100); + for (wxSpinCtrl *spin : m_strength_spins) + if (spin) + spin->SetValue(100); + } + + wxChoice *m_options_tab_choice {nullptr}; + wxSimplebook *m_options_book {nullptr}; + wxChoice *m_texture_mapping_mode_choice {nullptr}; + wxSpinCtrlDouble *m_tone_gamma_spin {nullptr}; + wxSpinCtrlDouble *m_sagging_ratio_spin {nullptr}; + wxSlider *m_preview_opacity_slider {nullptr}; + wxSpinCtrl *m_preview_opacity_spin {nullptr}; + wxCheckBox *m_force_sequential_filaments_checkbox {nullptr}; + wxCheckBox *m_auto_adjust_filament_selection_checkbox {nullptr}; + wxCheckBox *m_preview_limit_resolution_checkbox {nullptr}; + wxCheckBox *m_reduce_outer_surface_texture_checkbox {nullptr}; + wxCheckBox *m_seam_hiding_checkbox {nullptr}; + wxCheckBox *m_nonlinear_offset_adjustment_checkbox {nullptr}; + wxCheckBox *m_compact_offset_mode_checkbox {nullptr}; + std::vector m_minimum_offset_sliders; + std::vector m_minimum_offset_spins; + std::vector m_strength_sliders; + std::vector m_strength_spins; +}; + } // namespace // Sidebar / private @@ -525,6 +1331,14 @@ struct Sidebar::priv wxScrolledWindow* m_panel_filament_content; wxScrolledWindow* m_scrolledWindow_filament_content; wxStaticLine* m_staticline2; + StaticBox* m_panel_texture_mapping_title = nullptr; + wxPanel* m_panel_texture_mapping_content = nullptr; + wxBoxSizer* m_sizer_texture_mapping_content = nullptr; + ScalableButton* m_texture_mapping_icon = nullptr; + wxStaticText* m_staticText_texture_mapping = nullptr; + Button* m_btn_add_texture_map = nullptr; + std::unordered_set m_expanded_texture_mapping_rows; + int m_texture_mapping_advanced_options_tab = 0; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; Button * m_flushing_volume_btn = nullptr; @@ -813,12 +1627,15 @@ std::vector get_min_flush_volumes(const DynamicPrintConfig &full_config, si struct DynamicFilamentList : DynamicList { std::vector> items; + std::vector values; void apply_on(Choice *c) override { + if (c == nullptr || c->window == nullptr) + return; if (items.empty()) update(true); - auto cb = dynamic_cast(c->window); + auto cb = static_cast(c->window); wxString old_selection = cb->GetStringSelection(); int old_index = cb->GetSelection(); cb->Clear(); @@ -844,27 +1661,55 @@ struct DynamicFilamentList : DynamicList wxString get_value(int index) override { wxString str; - str << index; + if (index >= 0 && size_t(index) < values.size()) + str << values[size_t(index)]; + else + str << index; return str; } int index_of(wxString value) override { long n = 0; - return (value.ToLong(&n) && n <= items.size()) ? int(n) : -1; + if (!value.ToLong(&n)) + return -1; + for (size_t idx = 0; idx < values.size(); ++idx) + if (values[idx] == int(n)) + return int(idx); + return -1; } void update(bool force = false) { items.clear(); + values.clear(); if (!force && m_choices.empty()) return; auto icons = get_extruder_color_icons(true); auto presets = wxGetApp().preset_bundle->filament_presets; + values.emplace_back(0); for (int i = 0; i < presets.size(); ++i) { wxString str; std::string type; wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); str << type; items.push_back({str, i < icons.size() ? icons[i] : nullptr}); + values.emplace_back(i + 1); + } + if (wxGetApp().preset_bundle != nullptr) { + PresetBundle *bundle = wxGetApp().preset_bundle; + if (const ConfigOptionStrings *colors = bundle->project_config.option("filament_colour")) { + const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? + bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + bundle->texture_mapping_zones.load_entries(serialized, colors->values); + } + for (const unsigned int zone_id : bundle->texture_mapping_zones.zone_ids_by_index()) { + if (zone_id == 0) + continue; + const TextureMappingZone *zone = bundle->texture_mapping_zones.zone_from_id(zone_id); + items.push_back({zone != nullptr ? texture_mapping_menu_label(*zone) : wxString::Format(_L("Texture Mapping %u"), zone_id), + zone_id >= 1 && zone_id <= icons.size() ? icons[size_t(zone_id - 1)] : nullptr}); + values.emplace_back(int(zone_id)); + } } DynamicList::update(); } @@ -874,13 +1719,15 @@ struct DynamicFilamentList1Based : DynamicFilamentList { void apply_on(Choice *c) override { + if (c == nullptr || c->window == nullptr) + return; if (items.empty()) update(true); - auto cb = dynamic_cast(c->window); + auto cb = static_cast(c->window); auto n = cb->GetSelection(); cb->Clear(); for (auto i : items) { - cb->Append(i.first, *i.second); + cb->Append(i.first, i.second ? *i.second : wxNullBitmap); } if (n < cb->GetCount()) cb->SetSelection(n); @@ -888,7 +1735,10 @@ struct DynamicFilamentList1Based : DynamicFilamentList wxString get_value(int index) override { wxString str; - str << index+1; + if (index >= 0 && size_t(index) < values.size()) + str << values[size_t(index)]; + else + str << index + 1; return str; } int index_of(wxString value) override @@ -896,12 +1746,15 @@ struct DynamicFilamentList1Based : DynamicFilamentList long n = 0; if(!value.ToLong(&n)) return -1; - --n; - return (n >= 0 && n <= items.size()) ? int(n) : -1; + for (size_t idx = 0; idx < values.size(); ++idx) + if (values[idx] == int(n)) + return int(idx); + return -1; } void update(bool force = false) { items.clear(); + values.clear(); if (!force && m_choices.empty()) return; auto icons = get_extruder_color_icons(true); @@ -912,6 +1765,24 @@ struct DynamicFilamentList1Based : DynamicFilamentList wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); str << type; items.push_back({str, i < icons.size() ? icons[i] : nullptr}); + values.emplace_back(i + 1); + } + if (wxGetApp().preset_bundle != nullptr) { + PresetBundle *bundle = wxGetApp().preset_bundle; + if (const ConfigOptionStrings *colors = bundle->project_config.option("filament_colour")) { + const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? + bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + bundle->texture_mapping_zones.load_entries(serialized, colors->values); + } + for (const unsigned int zone_id : bundle->texture_mapping_zones.zone_ids_by_index()) { + if (zone_id == 0) + continue; + const TextureMappingZone *zone = bundle->texture_mapping_zones.zone_from_id(zone_id); + items.push_back({zone != nullptr ? texture_mapping_menu_label(*zone) : wxString::Format(_L("Texture Mapping %u"), zone_id), + zone_id >= 1 && zone_id <= icons.size() ? icons[size_t(zone_id - 1)] : nullptr}); + values.emplace_back(int(zone_id)); + } } DynamicList::update(); } @@ -2201,6 +3072,105 @@ Sidebar::Sidebar(Plater *parent) scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament } + { + p->m_panel_texture_mapping_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_texture_mapping_title->SetBackgroundColor(title_bg); + p->m_panel_texture_mapping_title->SetBackgroundColor2(0xF1F1F1); + + p->m_texture_mapping_icon = new ScalableButton(p->m_panel_texture_mapping_title, wxID_ANY, "param_flush"); + p->m_staticText_texture_mapping = new Label(p->m_panel_texture_mapping_title, _L("Texture Mapping Zones"), LB_PROPAGATE_MOUSE_EVENT); + + auto persist_texture_mapping = [this]() { + PresetBundle *bundle = wxGetApp().preset_bundle; + if (bundle == nullptr) + return; + const std::string serialized = bundle->texture_mapping_zones.serialize_entries(); + DynamicPrintConfig *print_cfg = &bundle->prints.get_edited_preset().config; + if (ConfigOptionString *opt = print_cfg->option("texture_mapping_definitions")) + opt->value = serialized; + else + print_cfg->set_key_value("texture_mapping_definitions", new ConfigOptionString(serialized)); + if (ConfigOptionString *opt = bundle->project_config.option("texture_mapping_definitions")) + opt->value = serialized; + else + bundle->project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(serialized)); + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe != nullptr) + wxGetApp().mainframe->on_config_changed(print_cfg); + if (wxGetApp().plater() != nullptr) + wxGetApp().plater()->update_project_dirty_from_presets(); + update_texture_mapping_panel(false); + update_dynamic_filament_list(); + }; + auto add_texture_map_action = [this, persist_texture_mapping]() { + PresetBundle *bundle = wxGetApp().preset_bundle; + if (bundle == nullptr) + return; + ConfigOptionStrings *colors_opt = bundle->project_config.option("filament_colour"); + std::vector colors = colors_opt ? colors_opt->values : std::vector(); + if (colors.size() < 2) + return; + const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? + bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + bundle->texture_mapping_zones.load_entries(serialized, colors); + bundle->texture_mapping_zones.add_zone(colors.size(), colors); + persist_texture_mapping(); + }; + + p->m_btn_add_texture_map = new Button(p->m_panel_texture_mapping_title, _L("Add Texture Mapping Zone")); + p->m_btn_add_texture_map->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + auto stop_texture_map_button_mouse = [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }; + p->m_btn_add_texture_map->Bind(wxEVT_LEFT_DOWN, stop_texture_map_button_mouse); + p->m_btn_add_texture_map->Bind(wxEVT_LEFT_UP, stop_texture_map_button_mouse); + p->m_btn_add_texture_map->Bind(wxEVT_BUTTON, [add_texture_map_action](wxCommandEvent &) { + add_texture_map_action(); + }); + + wxBoxSizer* h_sizer_texture_title = new wxBoxSizer(wxHORIZONTAL); + h_sizer_texture_title->Add(p->m_texture_mapping_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_texture_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_sizer_texture_title->Add(p->m_staticText_texture_mapping, 0, wxALIGN_CENTER); + h_sizer_texture_title->AddStretchSpacer(); + h_sizer_texture_title->Add(p->m_btn_add_texture_map, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_texture_title->SetMinSize(-1, FromDIP(30)); + p->m_panel_texture_mapping_title->SetSizer(h_sizer_texture_title); + p->m_panel_texture_mapping_title->Layout(); + + auto spliter_texture_1 = new ::StaticLine(p->scrolled); + spliter_texture_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_texture_1, 0, wxEXPAND); + scrolled_sizer->Add(p->m_panel_texture_mapping_title, 0, wxEXPAND | wxALL, 0); + auto spliter_texture_2 = new ::StaticLine(p->scrolled); + spliter_texture_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_texture_2, 0, wxEXPAND); + + p->m_panel_texture_mapping_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_panel_texture_mapping_content->SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(45, 45, 49) : wxColour(255, 255, 255)); + p->m_sizer_texture_mapping_content = new wxBoxSizer(wxVERTICAL); + p->m_sizer_texture_mapping_content->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + p->m_panel_texture_mapping_content->SetSizer(p->m_sizer_texture_mapping_content); + p->m_panel_texture_mapping_content->Layout(); + scrolled_sizer->Add(p->m_panel_texture_mapping_content, 0, wxEXPAND, 0); + + p->m_panel_texture_mapping_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { + int button_left = p->m_panel_texture_mapping_title->GetClientSize().x; + if (p->m_btn_add_texture_map && p->m_btn_add_texture_map->IsShown()) + button_left = std::min(button_left, p->m_btn_add_texture_map->GetPosition().x); + if (e.GetPosition().x > button_left - FromDIP(12)) + return; + p->m_panel_texture_mapping_content->Show(!p->m_panel_texture_mapping_content->IsShown()); + m_scrolled_sizer->Layout(); + }); + + p->m_panel_texture_mapping_title->Hide(); + p->m_panel_texture_mapping_content->Hide(); + } + { //add project title auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; @@ -2292,6 +3262,7 @@ Sidebar::Sidebar(Plater *parent) auto *sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(p->scrolled, 1, wxEXPAND); SetSizer(sizer); + update_texture_mapping_panel(); } Sidebar::~Sidebar() {} @@ -2586,6 +3557,7 @@ void Sidebar::update_presets(Preset::Type preset_type) p->combos_filament[i]->update(); update_dynamic_filament_list(); + update_texture_mapping_panel(); break; } @@ -3109,6 +4081,7 @@ void Sidebar::on_filament_count_change(size_t num_filaments) p->m_panel_filament_title->Refresh(); update_ui_from_settings(); update_dynamic_filament_list(); + update_texture_mapping_panel(); } void Sidebar::on_filaments_delete(size_t filament_id) @@ -3168,7 +4141,8 @@ void Sidebar::on_filaments_delete(size_t filament_id) Layout(); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - dynamic_filament_list.update(); + update_dynamic_filament_list(); + update_texture_mapping_panel(); } void Sidebar::add_filament() { @@ -3733,6 +4707,524 @@ void Sidebar::update_dynamic_filament_list() dynamic_filament_list_1_based.update(); } +void Sidebar::update_texture_mapping_panel(bool sync_manager) +{ + if (p->m_panel_texture_mapping_title == nullptr || p->m_panel_texture_mapping_content == nullptr) + return; + + wxWindowUpdateLocker no_updates_sidebar(this); + wxWindowUpdateLocker no_updates_panel(p->m_panel_texture_mapping_content); + + PresetBundle *bundle = wxGetApp().preset_bundle; + if (bundle == nullptr) + return; + + DynamicPrintConfig *print_cfg = &bundle->prints.get_edited_preset().config; + ConfigOptionStrings *color_opt = bundle->project_config.option("filament_colour"); + const size_t num_physical = color_opt ? color_opt->values.size() : p->combos_filament.size(); + std::vector physical_colors = color_opt ? color_opt->values : std::vector(); + physical_colors.resize(num_physical, "#26A69A"); + + std::vector nozzle_diameters(num_physical, 0.4); + if (const ConfigOptionFloats *opt = bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { + const size_t opt_count = opt->values.size(); + if (opt_count > 0) { + for (size_t i = 0; i < num_physical; ++i) + nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); + } + } + + auto get_config_string = [bundle, print_cfg](const std::string &key) { + if (bundle->project_config.has(key)) { + const std::string value = bundle->project_config.opt_string(key); + if (!value.empty()) + return value; + } + if (print_cfg != nullptr && print_cfg->has(key)) + return print_cfg->opt_string(key); + return std::string(); + }; + + auto set_config_string = [bundle, print_cfg](const std::string &key, const std::string &value) { + if (print_cfg != nullptr) { + if (ConfigOptionString *opt = print_cfg->option(key)) + opt->value = value; + else + print_cfg->set_key_value(key, new ConfigOptionString(value)); + } + if (ConfigOptionString *opt = bundle->project_config.option(key)) + opt->value = value; + else + bundle->project_config.set_key_value(key, new ConfigOptionString(value)); + }; + + auto notify_change = [this, print_cfg]() { + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe != nullptr && print_cfg != nullptr) + wxGetApp().mainframe->on_config_changed(print_cfg); + if (wxGetApp().plater() != nullptr) { + wxGetApp().plater()->update_project_dirty_from_presets(); + if (wxGetApp().plater()->get_view3D_canvas3D() != nullptr) + wxGetApp().plater()->get_view3D_canvas3D()->reload_scene(false); + } + update_dynamic_filament_list(); + if (obj_list() != nullptr) + obj_list()->update_filament_colors(); + }; + + TextureMappingManager &mgr = bundle->texture_mapping_zones; + TextureMappingManager *mgr_ptr = &mgr; + if (sync_manager) + mgr.load_entries(get_config_string("texture_mapping_definitions"), physical_colors); + for (TextureMappingZone &zone : mgr.zones()) + if (!zone.deleted) + zone.enabled = true; + mgr.normalize_zone_ids(num_physical); + set_config_string("texture_mapping_definitions", mgr.serialize_entries()); + + wxSizer *content_sizer = p->m_panel_texture_mapping_content->GetSizer(); + if (content_sizer == nullptr) + return; + content_sizer->Clear(true); + content_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + + if (p->m_btn_add_texture_map != nullptr) + p->m_btn_add_texture_map->Enable(num_physical >= 2); + + if (num_physical < 2) { + p->m_panel_texture_mapping_title->Hide(); + p->m_panel_texture_mapping_content->Hide(); + m_scrolled_sizer->Layout(); + Layout(); + return; + } + + p->m_panel_texture_mapping_title->Show(); + p->m_panel_texture_mapping_content->Show(); + + const bool is_dark = wxGetApp().dark_mode(); + const wxColour rows_bg = is_dark ? wxColour(45, 45, 49) : wxColour(246, 248, 251); + const wxColour row_bg = is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255); + const wxColour text_fg = is_dark ? wxColour(232, 232, 232) : wxColour(20, 20, 20); + const wxColour summary_fg = is_dark ? wxColour(182, 182, 182) : wxColour(96, 96, 96); + const int gap = FromDIP(6); + p->m_panel_texture_mapping_content->SetBackgroundColour(rows_bg); + + std::vector palette; + palette.reserve(physical_colors.size()); + for (const std::string &color : physical_colors) + palette.emplace_back(parse_texture_mapping_color(color)); + + std::vector visible_zone_indices; + std::vector zone_id_by_index = mgr.zone_ids_by_index(); + auto &zones = mgr.zones(); + for (size_t idx = 0; idx < zones.size(); ++idx) + if (!zones[idx].deleted) + visible_zone_indices.emplace_back(idx); + + if (visible_zone_indices.empty()) { + auto *empty_label = new wxStaticText(p->m_panel_texture_mapping_content, wxID_ANY, _L("No texture maps yet.")); + empty_label->SetForegroundColour(summary_fg); + empty_label->Wrap(FromDIP(360)); + content_sizer->Add(empty_label, 0, wxALL | wxEXPAND, FromDIP(12)); + p->m_panel_texture_mapping_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + return; + } + + auto persist_rows = [mgr_ptr, set_config_string, notify_change]() { + set_config_string("texture_mapping_definitions", mgr_ptr->serialize_entries()); + notify_change(); + }; + + for (const size_t zone_index : visible_zone_indices) { + if (zone_index >= zones.size()) + continue; + TextureMappingZone &entry = zones[zone_index]; + if (entry.display_color.empty() || entry.display_color[0] != '#') + entry.display_color = wxString::Format("#%06X", unsigned((entry.stable_id * 2654435761u) & 0xFFFFFFu)).ToStdString(); + + auto *row = new wxPanel(p->m_panel_texture_mapping_content, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + row->SetBackgroundColour(row_bg); + auto *row_sizer = new wxBoxSizer(wxVERTICAL); + row->SetSizer(row_sizer); + + auto *header = new wxPanel(row, wxID_ANY); + header->SetBackgroundColour(row_bg); + auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); + header->SetSizer(header_sizer); + + const unsigned int zone_id = zone_index < zone_id_by_index.size() && zone_id_by_index[zone_index] != 0 ? + zone_id_by_index[zone_index] : unsigned(num_physical + 1); + + auto *swatch = new TextureMappingNumberSwatch(header); + swatch->SetBackgroundColour(row_bg); + swatch->set_data(parse_texture_mapping_color(entry.display_color), zone_id); + header_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + auto *title = new wxStaticText(header, wxID_ANY, _L("Texture Mapping")); + title->SetForegroundColour(text_fg); + header_sizer->Add(title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + auto *summary = new wxStaticText(header, wxID_ANY, texture_mapping_summary(entry, num_physical)); + summary->SetForegroundColour(summary_fg); + header_sizer->Add(summary, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + auto *preview = new TextureMappingPatternPreview(header); + preview->SetBackgroundColour(row_bg); + preview->set_data(palette, + entry.is_image_texture() ? + TextureMappingManager::effective_texture_component_ids(entry, num_physical, physical_colors) : + texture_mapping_selected_ids(entry, num_physical), + parse_texture_mapping_color(entry.display_color)); + header_sizer->Add(preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + + auto *menu_btn = new ScalableButton(header, wxID_ANY, "menu_filament"); + menu_btn->SetToolTip(_L("Texture map actions")); + header_sizer->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + row_sizer->Add(header, 0, wxEXPAND | wxTOP | wxBOTTOM, gap); + + auto *editor = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + editor->SetBackgroundColour(row_bg); + auto *editor_sizer = new wxBoxSizer(wxVERTICAL); + editor->SetSizer(editor_sizer); + row_sizer->Add(editor, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + + const bool expanded = p->m_expanded_texture_mapping_rows.count(zone_index) != 0; + editor->Show(expanded); + + auto refresh_summary_preview = [summary, preview, swatch, zone_id, num_physical, physical_colors, palette](const TextureMappingZone &zone) { + if (summary != nullptr) + summary->SetLabel(texture_mapping_summary(zone, num_physical)); + if (preview != nullptr) { + preview->set_data(palette, + zone.is_image_texture() ? + TextureMappingManager::effective_texture_component_ids(zone, num_physical, physical_colors) : + texture_mapping_selected_ids(zone, num_physical), + parse_texture_mapping_color(zone.display_color)); + } + if (swatch != nullptr) + swatch->set_data(parse_texture_mapping_color(zone.display_color), zone_id); + }; + + auto apply_zone = [zone_index, mgr_ptr, num_physical, persist_rows, refresh_summary_preview](TextureMappingZone updated) { + auto &rows = mgr_ptr->zones(); + if (zone_index >= rows.size()) + return; + rows[zone_index] = std::move(updated); + mgr_ptr->normalize_zone_ids(num_physical); + refresh_summary_preview(rows[zone_index]); + persist_rows(); + }; + + auto *surface_row = new wxBoxSizer(wxHORIZONTAL); + surface_row->Add(new wxStaticText(editor, wxID_ANY, _L("Surface Pattern")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + wxArrayString surface_choices; + surface_choices.Add(_L("Image Texture")); + surface_choices.Add(_L("2D Gradient")); + auto *surface_choice = new wxChoice(editor, wxID_ANY, wxDefaultPosition, wxDefaultSize, surface_choices); + surface_choice->SetSelection(entry.is_2d_gradient() ? 1 : 0); + surface_row->Add(surface_choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + editor_sizer->Add(surface_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + auto *filaments_row = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); + filaments_row->Add(new wxStaticText(editor, wxID_ANY, _L("Filaments")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + std::vector filament_checks; + const std::vector selected_ids = texture_mapping_selected_ids(entry, num_physical); + for (size_t i = 1; i <= std::min(num_physical, 9); ++i) { + auto *chk = new wxCheckBox(editor, wxID_ANY, wxString::Format("F%d", int(i))); + chk->SetForegroundColour(text_fg); + chk->SetValue(std::find(selected_ids.begin(), selected_ids.end(), unsigned(i)) != selected_ids.end()); + filament_checks.emplace_back(chk); + filaments_row->Add(chk, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(2), gap / 2)); + } + editor_sizer->Add(filaments_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + auto *mode_row = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); + mode_row->Add(new wxStaticText(editor, wxID_ANY, _L("Filament colors")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *mode_choice = new wxChoice(editor, wxID_ANY, wxDefaultPosition, wxDefaultSize, texture_mapping_color_mode_choices()); + mode_choice->SetSelection(std::clamp(entry.filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorBW))); + mode_row->Add(mode_choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *preview_colors_chk = new wxCheckBox(editor, wxID_ANY, _L("Preview Result Colors")); + preview_colors_chk->SetValue(entry.preview_simulate_colors); + mode_row->Add(preview_colors_chk, 0, wxALIGN_CENTER_VERTICAL); + editor_sizer->Add(mode_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + auto *contrast_row = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); + contrast_row->Add(new wxStaticText(editor, wxID_ANY, _L("Texture contrast")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *contrast_spin = new wxSpinCtrl(editor, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(72), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, + 25, 300, std::clamp(int(std::lround(entry.contrast_pct)), 25, 300)); + contrast_row->Add(contrast_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + contrast_row->Add(new wxStaticText(editor, wxID_ANY, _L("%")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *high_res_chk = new wxCheckBox(editor, wxID_ANY, _L("High-resolution texture sampling")); + high_res_chk->SetValue(entry.high_resolution_sampling); + contrast_row->Add(high_res_chk, 0, wxALIGN_CENTER_VERTICAL); + editor_sizer->Add(contrast_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + + auto *button_row = new wxBoxSizer(wxHORIZONTAL); + auto *offset_btn = new wxButton(editor, wxID_ANY, _L("Offset Gradient Settings")); + auto *advanced_btn = new wxButton(editor, wxID_ANY, _L("Advanced Options")); + button_row->Add(offset_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + button_row->Add(advanced_btn, 0, wxALIGN_CENTER_VERTICAL); + button_row->AddStretchSpacer(1); + editor_sizer->Add(button_row, 0, wxEXPAND | wxALL, gap); + + auto apply_controls = [zone_index, mgr_ptr, num_physical, filament_checks, surface_choice, + mode_choice, preview_colors_chk, contrast_spin, high_res_chk, apply_zone]() { + auto &rows = mgr_ptr->zones(); + if (zone_index >= rows.size()) + return; + TextureMappingZone updated = rows[zone_index]; + std::vector ids; + for (size_t idx = 0; idx < filament_checks.size(); ++idx) + if (filament_checks[idx] != nullptr && filament_checks[idx]->GetValue()) + ids.emplace_back(unsigned(idx + 1)); + if (ids.size() < 2) + ids = texture_mapping_selected_ids(updated, num_physical); + updated.enabled = true; + updated.surface_pattern = surface_choice != nullptr && surface_choice->GetSelection() == 1 ? + int(TextureMappingZone::Gradient2D) : int(TextureMappingZone::ImageTexture); + updated.component_ids = encode_texture_mapping_component_ids(ids); + updated.component_a = ids.empty() ? 1 : ids.front(); + updated.component_b = ids.size() > 1 ? ids[1] : updated.component_a; + updated.filament_color_mode = mode_choice != nullptr ? + std::clamp(mode_choice->GetSelection(), int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorBW)) : + updated.filament_color_mode; + updated.preview_simulate_colors = preview_colors_chk != nullptr && preview_colors_chk->GetValue(); + updated.contrast_pct = contrast_spin != nullptr ? std::clamp(float(contrast_spin->GetValue()), 25.f, 300.f) : updated.contrast_pct; + updated.high_resolution_sampling = high_res_chk == nullptr || high_res_chk->GetValue(); + apply_zone(std::move(updated)); + }; + + contrast_spin->Bind(wxEVT_CHAR_HOOK, [contrast_spin, apply_controls](wxKeyEvent &evt) { + const int key = evt.GetKeyCode(); + if (key == WXK_UP || key == WXK_NUMPAD_UP || key == WXK_DOWN || key == WXK_NUMPAD_DOWN) { + const int direction = (key == WXK_UP || key == WXK_NUMPAD_UP) ? 1 : -1; + contrast_spin->SetValue(std::clamp(contrast_spin->GetValue() + direction * 5, 25, 300)); + apply_controls(); + return; + } + evt.Skip(); + }); + auto update_pattern_visibility = [this, editor_sizer, mode_row, contrast_row, offset_btn, advanced_btn, surface_choice, row, editor]() { + const bool image_texture = surface_choice == nullptr || surface_choice->GetSelection() == 0; + editor_sizer->Show(mode_row, image_texture, true); + editor_sizer->Show(contrast_row, image_texture, true); + if (offset_btn != nullptr) + offset_btn->Show(!image_texture); + if (advanced_btn != nullptr) + advanced_btn->Show(image_texture); + editor->Layout(); + row->Layout(); + p->m_panel_texture_mapping_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + }; + update_pattern_visibility(); + + surface_choice->Bind(wxEVT_CHOICE, [update_pattern_visibility, apply_controls](wxCommandEvent &) { + update_pattern_visibility(); + apply_controls(); + }); + for (wxCheckBox *chk : filament_checks) + if (chk != nullptr) + chk->Bind(wxEVT_CHECKBOX, [apply_controls](wxCommandEvent &) { apply_controls(); }); + mode_choice->Bind(wxEVT_CHOICE, [this, zone_index, mgr_ptr, num_physical, physical_colors, filament_checks, mode_choice, apply_controls](wxCommandEvent &) { + if (zone_index < mgr_ptr->zones().size()) { + TextureMappingZone &zone = mgr_ptr->zones()[zone_index]; + if (zone.auto_adjust_filament_selection) { + TextureMappingZone adjusted = zone; + adjusted.filament_color_mode = std::clamp(mode_choice != nullptr ? mode_choice->GetSelection() : zone.filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW)); + TextureMappingManager::auto_adjust_texture_component_ids(adjusted, num_physical, physical_colors); + const std::vector adjusted_ids = texture_mapping_selected_ids(adjusted, num_physical); + for (size_t idx = 0; idx < filament_checks.size(); ++idx) + if (filament_checks[idx] != nullptr) + filament_checks[idx]->SetValue(std::find(adjusted_ids.begin(), adjusted_ids.end(), unsigned(idx + 1)) != adjusted_ids.end()); + } + } + apply_controls(); + update_texture_mapping_panel(false); + }); + preview_colors_chk->Bind(wxEVT_CHECKBOX, [apply_controls](wxCommandEvent &) { apply_controls(); }); + high_res_chk->Bind(wxEVT_CHECKBOX, [apply_controls](wxCommandEvent &) { apply_controls(); }); + contrast_spin->Bind(wxEVT_SPINCTRL, [apply_controls](wxSpinEvent &) { apply_controls(); }); + contrast_spin->Bind(wxEVT_TEXT_ENTER, [apply_controls](wxCommandEvent &) { apply_controls(); }); + contrast_spin->Bind(wxEVT_KILL_FOCUS, [apply_controls](wxFocusEvent &evt) { + apply_controls(); + evt.Skip(); + }); + offset_btn->Bind(wxEVT_BUTTON, [this, zone_index, mgr_ptr, palette, nozzle_diameters, apply_zone](wxCommandEvent &) { + if (zone_index >= mgr_ptr->zones().size()) + return; + TextureMappingZone updated = mgr_ptr->zones()[zone_index]; + TextureMappingOffsetGradientDialog dlg(this, updated, palette.size(), nozzle_diameters, palette); + if (dlg.ShowModal() != wxID_OK || !dlg.apply_to(updated)) + return; + updated.surface_pattern = int(TextureMappingZone::Gradient2D); + apply_zone(std::move(updated)); + update_texture_mapping_panel(false); + }); + advanced_btn->Bind(wxEVT_BUTTON, [this, zone_index, mgr_ptr, palette, apply_zone](wxCommandEvent &) { + if (zone_index >= mgr_ptr->zones().size()) + return; + TextureMappingZone updated = mgr_ptr->zones()[zone_index]; + const std::vector ids = texture_mapping_selected_ids(updated, palette.size()); + std::vector strengths; + std::vector offsets; + strengths.reserve(ids.size()); + offsets.reserve(ids.size()); + for (const unsigned int id : ids) { + const size_t idx = id > 0 ? size_t(id - 1) : size_t(0); + strengths.emplace_back(idx < updated.filament_strengths_pct.size() ? updated.filament_strengths_pct[idx] : 100.f); + offsets.emplace_back(idx < updated.filament_minimum_offsets_pct.size() ? updated.filament_minimum_offsets_pct[idx] : 0.f); + } + TextureMappingAdvancedOptionsDialog dlg(this, + updated.texture_mapping_mode, + updated.filament_color_mode, + ids, + strengths, + offsets, + updated.tone_gamma, + updated.sagging_ratio, + updated.preview_opacity_pct, + updated.force_sequential_filaments, + updated.auto_adjust_filament_selection, + updated.preview_limit_resolution, + updated.reduce_outer_surface_texture, + updated.seam_hiding, + updated.nonlinear_offset_adjustment, + updated.compact_offset_mode, + p->m_texture_mapping_advanced_options_tab); + const int result = dlg.ShowModal(); + p->m_texture_mapping_advanced_options_tab = dlg.selected_options_tab(); + if (result != wxID_OK) + return; + updated.texture_mapping_mode = dlg.texture_mapping_mode(); + updated.tone_gamma = dlg.tone_gamma(); + updated.sagging_ratio = dlg.sagging_ratio(); + updated.preview_opacity_pct = dlg.preview_opacity_pct(); + updated.force_sequential_filaments = dlg.force_sequential_filaments(); + updated.auto_adjust_filament_selection = dlg.auto_adjust_filament_selection(); + updated.preview_limit_resolution = dlg.preview_limit_resolution(); + updated.reduce_outer_surface_texture = dlg.reduce_outer_surface_texture(); + updated.seam_hiding = dlg.seam_hiding(); + updated.nonlinear_offset_adjustment = dlg.nonlinear_offset_adjustment(); + updated.compact_offset_mode = dlg.compact_offset_mode(); + if (updated.filament_strengths_pct.size() < palette.size()) + updated.filament_strengths_pct.resize(palette.size(), 100.f); + const std::vector dlg_strengths = dlg.component_strengths_pct(); + for (size_t i = 0; i < ids.size() && i < dlg_strengths.size(); ++i) + if (ids[i] > 0 && size_t(ids[i] - 1) < updated.filament_strengths_pct.size()) + updated.filament_strengths_pct[size_t(ids[i] - 1)] = dlg_strengths[i]; + while (!updated.filament_strengths_pct.empty() && std::abs(updated.filament_strengths_pct.back() - 100.f) <= 1e-6f) + updated.filament_strengths_pct.pop_back(); + if (updated.filament_minimum_offsets_pct.size() < palette.size()) + updated.filament_minimum_offsets_pct.resize(palette.size(), 0.f); + const std::vector dlg_offsets = dlg.component_minimum_offsets_pct(); + for (size_t i = 0; i < ids.size() && i < dlg_offsets.size(); ++i) + if (ids[i] > 0 && size_t(ids[i] - 1) < updated.filament_minimum_offsets_pct.size()) + updated.filament_minimum_offsets_pct[size_t(ids[i] - 1)] = dlg_offsets[i]; + while (!updated.filament_minimum_offsets_pct.empty() && std::abs(updated.filament_minimum_offsets_pct.back()) <= 1e-6f) + updated.filament_minimum_offsets_pct.pop_back(); + apply_zone(std::move(updated)); + update_texture_mapping_panel(false); + }); + + auto toggle_editor = [this, zone_index, editor, row]() { + if (editor == nullptr) + return; + if (editor->IsShown()) { + editor->Hide(); + p->m_expanded_texture_mapping_rows.erase(zone_index); + } else { + editor->Show(); + p->m_expanded_texture_mapping_rows.insert(zone_index); + } + row->Layout(); + p->m_panel_texture_mapping_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + }; + auto bind_toggle = [toggle_editor](wxWindow *win) { + if (win == nullptr) + return; + win->SetCursor(wxCursor(wxCURSOR_HAND)); + win->Bind(wxEVT_LEFT_UP, [toggle_editor](wxMouseEvent &) { toggle_editor(); }); + }; + bind_toggle(header); + bind_toggle(title); + bind_toggle(summary); + bind_toggle(swatch); + bind_toggle(preview); + + menu_btn->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }); + menu_btn->Bind(wxEVT_BUTTON, [this, zone_index, num_physical, physical_colors, mgr_ptr, persist_rows, menu_btn](wxCommandEvent &) { + if (menu_btn == nullptr) + return; + wxMenu menu; + const int duplicate_id = wxWindow::NewControlId(); + const int delete_id = wxWindow::NewControlId(); + menu.Append(duplicate_id, _L("Duplicate")); + menu.Append(delete_id, _L("Delete")); + menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [this, zone_index, num_physical, physical_colors, mgr_ptr, persist_rows, duplicate_id, delete_id](wxCommandEvent &evt) { + auto &rows = mgr_ptr->zones(); + if (zone_index >= rows.size()) + return; + if (evt.GetId() == duplicate_id) { + mgr_ptr->duplicate_zone(zone_index, num_physical, physical_colors); + persist_rows(); + update_texture_mapping_panel(false); + return; + } + if (evt.GetId() == delete_id) { + rows.erase(rows.begin() + ptrdiff_t(zone_index)); + p->m_expanded_texture_mapping_rows.clear(); + persist_rows(); + update_texture_mapping_panel(false); + } + }); + menu_btn->PopupMenu(&menu, wxPoint(0, menu_btn->GetSize().GetHeight())); + }); + + content_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2)); + } + + content_sizer->AddSpacer(FromDIP(2)); + p->m_panel_texture_mapping_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); +} + +std::vector Sidebar::get_ui_ordered_filament_ids() const +{ + std::vector ordered_filament_ids; + const size_t num_physical = p->combos_filament.size(); + ordered_filament_ids.reserve(num_physical + 8); + for (size_t i = 0; i < num_physical; ++i) + ordered_filament_ids.emplace_back(unsigned(i + 1)); + if (wxGetApp().preset_bundle != nullptr) { + PresetBundle *bundle = wxGetApp().preset_bundle; + if (const ConfigOptionStrings *colors = bundle->project_config.option("filament_colour")) { + const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? + bundle->project_config.opt_string("texture_mapping_definitions") : + std::string(); + bundle->texture_mapping_zones.load_entries(serialized, colors->values); + } + for (const unsigned int zone_id : wxGetApp().preset_bundle->texture_mapping_zones.zone_ids_by_index()) + if (zone_id != 0) + ordered_filament_ids.emplace_back(zone_id); + } + return ordered_filament_ids; +} + PlaterPresetComboBox* Sidebar::printer_combox() { return p->combo_printer; @@ -4124,7 +5616,7 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int int m_max_flush_volume = Slic3r::g_max_flush_volume; unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); std::vector> multi_colours; // Support for multi-color filament @@ -6476,12 +7968,19 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { if (!boost::iends_with(path.string(), ".obj")) { return; } - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } }; + auto obj_import_mode_fun = [&path](const ObjImportCapabilities &capabilities) -> ObjImportMode { + if (!boost::iends_with(path.string(), ".obj")) + return ObjImportMode::UseDefault; + if (capabilities.texture_count > 0 && capabilities.has_valid_texture_uvs) + return ObjImportMode::ImportTextures; + return ObjImportMode::UseDefault; + }; if (boost::iends_with(path.string(), ".stp") || boost::iends_with(path.string(), ".step")) { double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_defletion")); @@ -6548,7 +8047,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ cont = dlg.Update(progress_percent, msg); cancel = !cont; }, - nullptr, 0, obj_color_fun); + nullptr, 0, obj_color_fun, obj_import_mode_fun); } if (designer_model_id.empty() && boost::algorithm::iends_with(path.string(), ".stl")) { @@ -6557,6 +8056,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (type_any_amf && is_xxx) imperial_units = true; + if (assign_imported_texture_mapping_zone(model)) { + sidebar->update_texture_mapping_panel(false); + sidebar->update_dynamic_filament_list(); + } + for (auto obj : model.objects) { if (obj->name.empty()) { obj->name = fs::path(obj->input_file).filename().string(); @@ -8670,12 +10174,19 @@ void Plater::priv::reload_from_disk() const auto& path = input_paths[i].string(); auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { if (!boost::iends_with(path, ".obj")) { return; } - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } }; + auto obj_import_mode_fun = [&path](const ObjImportCapabilities &capabilities) -> ObjImportMode { + if (!boost::iends_with(path, ".obj")) + return ObjImportMode::UseDefault; + if (capabilities.texture_count > 0 && capabilities.has_valid_texture_uvs) + return ObjImportMode::ImportTextures; + return ObjImportMode::UseDefault; + }; wxBusyCursor wait; wxBusyInfo info(_L("Reload from:") + " " + from_u8(path), q->get_current_canvas3D()->get_wxglcanvas()); @@ -8695,9 +10206,13 @@ void Plater::priv::reload_from_disk() bool is_split = wxGetApp().app_config->get_bool("is_split_compound"); new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, nullptr, linear, angle, is_split); }else { - new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, obj_color_fun); + new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, obj_color_fun, obj_import_mode_fun); } + if (assign_imported_texture_mapping_zone(new_model)) { + sidebar->update_texture_mapping_panel(false); + sidebar->update_dynamic_filament_list(); + } for (ModelObject* model_object : new_model.objects) { @@ -16284,6 +17799,56 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r { // only update elements in plater update_filament_colors_in_full_config(); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + auto is_texture_mapping_zone = [preset_bundle](int filament_id_1based) { + return filament_id_1based > 0 && + preset_bundle != nullptr && + preset_bundle->texture_mapping_zones.is_texture_mapping_zone_id(unsigned(filament_id_1based)); + }; + auto remap_optional_physical_filament = [filament_id, is_texture_mapping_zone](int filament_id_1based) { + if (filament_id_1based <= 0 || is_texture_mapping_zone(filament_id_1based)) + return filament_id_1based; + if (size_t(filament_id_1based) == filament_id + 1) + return 0; + if (size_t(filament_id_1based) > filament_id + 1) + return filament_id_1based - 1; + return filament_id_1based; + }; + auto remap_custom_gcode_filament = [filament_id, replace_filament_id, is_texture_mapping_zone](int filament_id_1based) { + if (filament_id_1based <= 0 || is_texture_mapping_zone(filament_id_1based)) + return filament_id_1based; + if (size_t(filament_id_1based) == filament_id + 1) + return replace_filament_id == -1 ? 0 : replace_filament_id + 1; + if (size_t(filament_id_1based) > filament_id + 1) + return filament_id_1based - 1; + return filament_id_1based; + }; + + if (preset_bundle != nullptr) { + DynamicPrintConfig &project_config = preset_bundle->project_config; + ConfigOptionStrings *color_opt = project_config.option("filament_colour"); + if (color_opt != nullptr) { + std::vector old_colors = color_opt->values; + const size_t insert_pos = std::min(filament_id, old_colors.size()); + old_colors.insert(old_colors.begin() + ptrdiff_t(insert_pos), "#26A69A"); + const std::string serialized = project_config.has("texture_mapping_definitions") ? + project_config.opt_string("texture_mapping_definitions") : + std::string(); + preset_bundle->texture_mapping_zones.load_entries(serialized, old_colors); + preset_bundle->texture_mapping_zones.remove_physical_filament(unsigned(filament_id + 1)); + preset_bundle->texture_mapping_zones.refresh(color_opt->values); + const std::string remapped = preset_bundle->texture_mapping_zones.serialize_entries(); + if (ConfigOptionString *opt = project_config.option("texture_mapping_definitions")) + opt->value = remapped; + else + project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(remapped)); + DynamicPrintConfig &print_config = preset_bundle->prints.get_edited_preset().config; + if (ConfigOptionString *opt = print_config.option("texture_mapping_definitions")) + opt->value = remapped; + else + print_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(remapped)); + } + } // update fisrt print sequence and other layer sequence //move to partplate->on_filament_deleted @@ -16307,12 +17872,11 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r static const char *keys[] = {"support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { - if(p->config->opt_int(key) == filament_id + 1) + const int new_value = remap_optional_physical_filament(p->config->opt_int(key)); + if (new_value == 0) (*(p->config)).erase(key); - else { - int new_value = p->config->opt_int(key) > filament_id ? p->config->opt_int(key) - 1 : p->config->opt_int(key); + else (*(p->config)).set_key_value(key, new ConfigOptionInt(new_value)); - } } // update object/volume/support(object and volume) filament id @@ -16320,8 +17884,8 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r // update customize gcode for (auto item = p->model.plates_custom_gcodes.begin(); item != p->model.plates_custom_gcodes.end(); ++item) { - auto iter = std::remove_if(item->second.gcodes.begin(), item->second.gcodes.end(), [filament_id](const Item& gcode_item) { - return (gcode_item.type == CustomGCode::Type::ToolChange && gcode_item.extruder == filament_id + 1); + auto iter = std::remove_if(item->second.gcodes.begin(), item->second.gcodes.end(), [remap_custom_gcode_filament](const Item& gcode_item) { + return gcode_item.type == CustomGCode::Type::ToolChange && remap_custom_gcode_filament(gcode_item.extruder) == 0; }); if (replace_filament_id == -1) item->second.gcodes.erase(iter, item->second.gcodes.end()); @@ -16330,10 +17894,11 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r } for (auto& item : item->second.gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder > filament_id) - item.extruder--; + if (item.type == CustomGCode::Type::ToolChange) + item.extruder = remap_custom_gcode_filament(item.extruder); } } + sidebar().update_texture_mapping_panel(false); } std::vector Plater::get_extruders_colors() @@ -16618,7 +18183,7 @@ void Plater::on_activate() } // Get vector of extruder colors considering filament color, if extruder color is undefined. -std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const +std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result, bool include_texture_mapping_zones) const { if (wxGetApp().is_gcode_viewer() && result != nullptr) return result->extruder_colors; @@ -16629,6 +18194,15 @@ std::vector Plater::get_extruder_colors_from_plater_config(const GC return filament_colors; filament_colors = (config->option("filament_colour"))->values; + if (!include_texture_mapping_zones) + return filament_colors; + const size_t num_physical = filament_colors.size(); + if (PresetBundle *bundle = wxGetApp().preset_bundle; bundle != nullptr) { + const std::string texture_mapping_definitions = config->has("texture_mapping_definitions") ? config->opt_string("texture_mapping_definitions") : std::string(); + bundle->texture_mapping_zones.load_entries(texture_mapping_definitions, filament_colors); + const std::vector zone_colors = bundle->texture_mapping_zones.display_colors(num_physical); + filament_colors.insert(filament_colors.end(), zone_colors.begin(), zone_colors.end()); + } return filament_colors; } } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 31e0e2f0e1..e7357352ca 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -181,6 +181,8 @@ public: // BBS. Add filament_added() method. void on_filament_count_change(size_t num_filaments); void on_filaments_delete(size_t filament_id); + void update_texture_mapping_panel(bool sync_manager = true); + std::vector get_ui_ordered_filament_ids() const; void add_filament(); void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default @@ -568,7 +570,7 @@ public: void force_print_bed_update(); // On activating the parent window. void on_activate(); - std::vector get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const; + std::vector get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr, bool include_texture_mapping_zones = true) const; std::vector get_filament_colors_render_info() const; std::vector get_filament_color_render_type() const; std::vector get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const; @@ -977,4 +979,4 @@ wxArrayString get_all_camera_view_type(); } // namespace GUI } // namespace Slic3r -#endif \ No newline at end of file +#endif diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8296c668c7..94fdefdc5c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1892,6 +1892,18 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) } } + if (m_type == Preset::TYPE_PRINT && + (opt_key == "texture_mapping_outer_wall_gradient_global_strength" || + opt_key == "texture_mapping_outer_wall_gradient_max_line_width" || + opt_key == "texture_mapping_outer_wall_gradient_min_line_width" || + opt_key == "texture_mapping_definitions")) { + DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config; + if (const ConfigOption *opt = m_config->option(opt_key)) + project_cfg.set_key_value(opt_key, opt->clone()); + if (wxGetApp().plater() != nullptr) + wxGetApp().sidebar().update_texture_mapping_panel(opt_key == "texture_mapping_definitions"); + } + if (m_postpone_update_ui) { // It means that not all values are rolled to the system/last saved values jet. // And call of the update() can causes a redundant check of the config values, @@ -2583,6 +2595,11 @@ void TabPrint::build() optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers"); optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower"); + optgroup = page->new_optgroup(L("Texture Mapping"), L"param_flush"); + optgroup->append_single_option_line("texture_mapping_outer_wall_gradient_global_strength"); + optgroup->append_single_option_line("texture_mapping_outer_wall_gradient_max_line_width"); + optgroup->append_single_option_line("texture_mapping_outer_wall_gradient_min_line_width"); + optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features"); optgroup->append_single_option_line("wall_filament", "multimaterial_settings_filament_for_features#walls"); optgroup->append_single_option_line("sparse_infill_filament", "multimaterial_settings_filament_for_features#infill"); diff --git a/src/slic3r/GUI/mrgtmp0 b/src/slic3r/GUI/mrgtmp0 deleted file mode 100644 index cf2de90d12..0000000000 --- a/src/slic3r/GUI/mrgtmp0 +++ /dev/null @@ -1,162 +0,0 @@ -#ifndef slic3r_WebViewDialog_hpp_ -#define slic3r_WebViewDialog_hpp_ - - -#include "wx/artprov.h" -#include "wx/cmdline.h" -#include "wx/notifmsg.h" -#include "wx/settings.h" -#include "wx/webview.h" - -#if wxUSE_WEBVIEW_EDGE -#include "wx/msw/webview_edge.h" -#endif - -#include "wx/webviewarchivehandler.h" -#include "wx/webviewfshandler.h" -#include "wx/numdlg.h" -#include "wx/infobar.h" -#include "wx/filesys.h" -#include "wx/fs_arc.h" -#include "wx/fs_mem.h" -#include "wx/stdpaths.h" -#include -#include -#include "wx/textctrl.h" - - -namespace Slic3r { -namespace GUI { - - -class WebViewPanel : public wxPanel -{ -public: - WebViewPanel(wxWindow *parent); - virtual ~WebViewPanel(); - - void load_url(wxString& url); - - void UpdateState(); - void OnIdle(wxIdleEvent& evt); - void OnUrl(wxCommandEvent& evt); - void OnBack(wxCommandEvent& evt); - void OnForward(wxCommandEvent& evt); - void OnStop(wxCommandEvent& evt); - void OnReload(wxCommandEvent& evt); - void OnNavigationRequest(wxWebViewEvent& evt); - void OnNavigationComplete(wxWebViewEvent& evt); - void OnDocumentLoaded(wxWebViewEvent& evt); - void OnTitleChanged(wxWebViewEvent &evt); - void OnNewWindow(wxWebViewEvent& evt); - void OnScriptMessage(wxWebViewEvent& evt); - void OnScriptResponseMessage(wxCommandEvent& evt); - void OnViewSourceRequest(wxCommandEvent& evt); - void OnViewTextRequest(wxCommandEvent& evt); - void OnToolsClicked(wxCommandEvent& evt); - void OnError(wxWebViewEvent& evt); - void OnCut(wxCommandEvent& evt); - void OnCopy(wxCommandEvent& evt); - void OnPaste(wxCommandEvent& evt); - void OnUndo(wxCommandEvent& evt); - void OnRedo(wxCommandEvent& evt); - void OnMode(wxCommandEvent& evt); - void RunScript(const wxString& javascript); - void OnRunScriptString(wxCommandEvent& evt); - void OnRunScriptInteger(wxCommandEvent& evt); - void OnRunScriptDouble(wxCommandEvent& evt); - void OnRunScriptBool(wxCommandEvent& evt); - void OnRunScriptObject(wxCommandEvent& evt); - void OnRunScriptArray(wxCommandEvent& evt); - void OnRunScriptDOM(wxCommandEvent& evt); - void OnRunScriptUndefined(wxCommandEvent& evt); - void OnRunScriptNull(wxCommandEvent& evt); - void OnRunScriptDate(wxCommandEvent& evt); - void OnRunScriptMessage(wxCommandEvent& evt); - void OnRunScriptCustom(wxCommandEvent& evt); - void OnAddUserScript(wxCommandEvent& evt); - void OnSetCustomUserAgent(wxCommandEvent& evt); - void OnClearSelection(wxCommandEvent& evt); - void OnDeleteSelection(wxCommandEvent& evt); - void OnSelectAll(wxCommandEvent& evt); - void OnLoadScheme(wxCommandEvent& evt); - void OnUseMemoryFS(wxCommandEvent& evt); - void OnEnableContextMenu(wxCommandEvent& evt); - void OnEnableDevTools(wxCommandEvent& evt); - void OnClose(wxCloseEvent& evt); - - wxTimer * m_LoginUpdateTimer{nullptr}; - void OnFreshLoginStatus(wxTimerEvent &event); - -private: - void SendRecentList(); - -public: - void SendRecentList(wxString const &sequence_id); - void SendLoginInfo(); - -private: - - wxWebView* m_browser; - wxBoxSizer *bSizer_toolbar; - wxButton * m_button_back; - wxButton * m_button_forward; - wxButton * m_button_stop; - wxButton * m_button_reload; - wxTextCtrl *m_url; - wxButton * m_button_tools; - - wxMenu* m_tools_menu; - wxMenuItem* m_tools_handle_navigation; - wxMenuItem* m_tools_handle_new_window; - wxMenuItem* m_edit_cut; - wxMenuItem* m_edit_copy; - wxMenuItem* m_edit_paste; - wxMenuItem* m_edit_undo; - wxMenuItem* m_edit_redo; - wxMenuItem* m_edit_mode; - wxMenuItem* m_scroll_line_up; - wxMenuItem* m_scroll_line_down; - wxMenuItem* m_scroll_page_up; - wxMenuItem* m_scroll_page_down; - wxMenuItem* m_script_string; - wxMenuItem* m_script_integer; - wxMenuItem* m_script_double; - wxMenuItem* m_script_bool; - wxMenuItem* m_script_object; - wxMenuItem* m_script_array; - wxMenuItem* m_script_dom; - wxMenuItem* m_script_undefined; - wxMenuItem* m_script_null; - wxMenuItem* m_script_date; - wxMenuItem* m_script_message; - wxMenuItem* m_script_custom; - wxMenuItem* m_selection_clear; - wxMenuItem* m_selection_delete; - wxMenuItem* m_context_menu; - wxMenuItem* m_dev_tools; - - wxInfoBar *m_info; - wxStaticText* m_info_text; - - long m_zoomFactor; - - // Last executed JavaScript snippet, for convenience. - wxString m_javascript; - wxString m_response_js; - - wxString m_bbl_user_agent; - - DECLARE_EVENT_TABLE() -}; - -class SourceViewDialog : public wxDialog -{ -public: - SourceViewDialog(wxWindow* parent, wxString source); -}; - -} // GUI -} // Slic3r - -#endif /* slic3r_Tab_hpp_ */ diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 66ad109fab..9e096f1399 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -806,19 +806,26 @@ void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl, // For ObjectList we use short extruder name (just a number) const bool use_full_item_name = dynamic_cast(parent) == nullptr; - int i = 0; - wxString str = _(L("Extruder")); - for (wxBitmap* bmp : icons) { - if (i == 0) { - if (!first_item.empty()) - (*ctrl)->Append(_(first_item), *bmp); - ++i; - } + if (!first_item.empty()) + (*ctrl)->Append(_(first_item), *icons.front()); + std::vector ordered_filament_ids; + if (Slic3r::GUI::wxGetApp().plater() != nullptr) + ordered_filament_ids = Slic3r::GUI::wxGetApp().plater()->sidebar().get_ui_ordered_filament_ids(); + if (ordered_filament_ids.empty()) { + ordered_filament_ids.reserve(icons.size()); + for (size_t idx = 0; idx < icons.size(); ++idx) + ordered_filament_ids.emplace_back(unsigned(idx + 1)); + } + + wxString str = _(L("Extruder")); + for (const unsigned int filament_id : ordered_filament_ids) { + if (filament_id == 0 || filament_id > icons.size()) + continue; + wxBitmap *bmp = icons[size_t(filament_id - 1)]; (*ctrl)->Append(use_full_item_name - ? Slic3r::GUI::from_u8((boost::format("%1% %2%") % str % i).str()) - : wxString::Format("%d", i), *bmp); - ++i; + ? Slic3r::GUI::from_u8((boost::format("%1% %2%") % str % filament_id).str()) + : wxString::Format("%u", filament_id), *bmp); } (*ctrl)->SetSelection(0); } @@ -1253,4 +1260,3 @@ void ImageTransientPopup::OnMouse(wxMouseEvent &event) -