Merge upstream main: color mixing feature and related fixes

Brings in 54 upstream commits, the bulk of them the BambuStudio-ported color
mixing / mixed filament subsystem (#15347) plus its follow-ups, along with the
Assimp-backed colored OBJ import, warning-policy build changes, and assorted
profile and localization updates.

Two conflicts, both "each side added at the same point", resolved by keeping
both:

- Print::validate() -- our IMEX multi-color block and upstream's new gradient
  mixed filament warning were inserted at the same spot after the empty
  extruders check. They test unrelated conditions, so both are kept, each with
  its own closing brace.
- tests/libslic3r/test_3mf.cpp -- our three IMEX per-plate round-trip scenarios
  and upstream's mixed-filament round-trip scenario both append to the end of
  the file, and each side added one include. All four scenarios and both
  includes are kept.

Everything else merged cleanly, including GCode.cpp, ToolOrdering.cpp,
PartPlate.cpp and PrintConfig.cpp. Upstream left the is_extruder_used block
untouched, so the IMEX supplement still applies, and estimate_wipe_tower_polygon
is unchanged, so the prime tower hull work is unaffected.

Not addressed here, and worth its own change: a mixed filament is a virtual slot
that no nozzle carries, while physical_extruder_map routes logical slots to
physical heads. Print::extruders() lists mixed slots under their own id whereas
tool_ordering.all_extruders() lists them post-expansion, so the IMEX pem lookups
have no defined answer for a mixed slot. Upstream's own guards reject a mixed
filament where a physical slot is required; IMEX likely wants the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-08-25 13:07:26 -04:00
co-authored by Claude Opus 5
171 changed files with 38575 additions and 638 deletions
+1
View File
@@ -2,6 +2,7 @@ Build
Build.bat
/build*/
CMakeLists.txt.user
CMakeUserPresets.json
**/CMakeLists.txt.autosave
deps/build*
MYMETA.json
+76 -5
View File
@@ -59,6 +59,13 @@ if (APPLE)
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif ()
# Keep MSVC's default /W3 out of CMAKE_<LANG>_FLAGS so it can be applied to our own
# targets only. Silencing a bundled target would otherwise override a warning level,
# which cl reports as D9025 for every file it compiles.
if (POLICY CMP0092)
cmake_policy(SET CMP0092 NEW)
endif ()
project(OrcaSlicer)
# Backward compatibility for old CMake versions
@@ -126,6 +133,8 @@ option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL,
option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0)
option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0)
option(SLIC3R_PCH "Use precompiled headers" 1)
option(SLIC3R_WARNINGS "Emit compiler warnings for OrcaSlicer sources" 1)
option(SLIC3R_BUNDLED_WARNINGS "Emit compiler warnings for bundled third-party sources" 0)
option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1)
option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1)
option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
@@ -335,14 +344,16 @@ if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
# clang-cl can interpret SYSTEM header paths if -imsvc is used
set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "-imsvc")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall \
-Wno-old-style-cast -Wno-reserved-id-macro -Wno-c++98-compat-pedantic")
else ()
set(IS_CLANG_CL FALSE)
endif ()
if (MSVC)
# CMP0092 only applies when the cache is created; an existing tree keeps its /W3,
# which a silenced bundled target would then override (D9025, once per file).
string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
# /MP only matters for the VS generators, where CMake turns it into the
# MultiProcessorCompilation property. Ninja parallelises on its own, and
# clang-cl warns "argument unused" if the flag reaches it.
@@ -526,8 +537,15 @@ if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fext-numeric-literals" )
endif()
if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
if (NOT MINGW)
if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
if (IS_CLANG_CL)
# clang-cl reads -Wall as MSVC /Wall, which clang maps to -Weverything. /W4 is
# its -Wall -Wextra and, unlike /clang:-Wall, is ordered with the -Wno-* below
# instead of after them. The -Wextra-only warnings are dropped again so the set
# matches what -Wall gives the GNU/Clang builds.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4" )
add_compile_options(-Wno-unused-parameter -Wno-ignored-qualifiers -Wno-missing-field-initializers)
elseif (NOT MINGW)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" )
endif ()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder" )
@@ -1089,8 +1107,57 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
endfunction()
# Bundled sources set their own warning flags, and a plain -Wall there means /Wall
# (= -Weverything) under clang-cl. Target options are applied after the ones a target
# set on itself, so these win. Targets are discovered rather than listed so a newly
# bundled library needs no maintenance here.
function(orcaslicer_silence_third_party_warnings _dir)
get_property(_subdirs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES)
foreach (_subdir IN LISTS _subdirs)
orcaslicer_silence_third_party_warnings("${_subdir}")
endforeach ()
get_property(_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS)
foreach (_target IN LISTS _targets)
get_target_property(_type ${_target} TYPE)
if (NOT _type STREQUAL "INTERFACE_LIBRARY" AND NOT _type STREQUAL "UTILITY")
if (MSVC AND NOT IS_CLANG_CL)
# Drop any level the target set for itself, or -w overrides it and cl
# reports D9025 once per file.
get_target_property(_opts ${_target} COMPILE_OPTIONS)
if (_opts)
string(REGEX REPLACE "/W[0-4]|/Wall" "" _opts "${_opts}")
string(REGEX REPLACE ";;+" ";" _opts "${_opts}")
set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}")
endif ()
# CMake maps a level into the VS generator's WarningLevel element, while a
# bare -w stays on the command line and trips D9025 there, once per file.
target_compile_options(${_target} PRIVATE /W0)
else ()
target_compile_options(${_target} PRIVATE -w)
endif ()
endif ()
endforeach ()
endfunction()
# libslic3r, OrcaSlicer GUI and the OrcaSlicer executable.
add_subdirectory(deps_src)
if (NOT SLIC3R_BUNDLED_WARNINGS)
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/deps_src")
endif ()
# Warning level for the targets added below: our sources, plus glad and libvgcode,
# which are vendored but live under src/. The deps_src libraries were configured just
# above. CMP0092 left MSVC without a default level, so it is set here.
if (NOT SLIC3R_WARNINGS)
add_compile_options(-w)
elseif (MSVC AND NOT IS_CLANG_CL)
# /we4715 is C4715, no return from a non-void function, matching the
# -Werror=return-type the GNU/Clang builds apply.
add_compile_options(/W3 /we4715)
endif ()
add_subdirectory(src)
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT OrcaSlicer_app_gui)
@@ -1102,6 +1169,10 @@ endif()
if(BUILD_TESTS)
add_subdirectory(tests)
if (NOT SLIC3R_BUNDLED_WARNINGS)
# Catch2 is vendored under tests/ and sets its own warning flags too.
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/tests/catch2")
endif ()
endif()
if (NOT WIN32 AND NOT APPLE)
+40
View File
@@ -0,0 +1,40 @@
if(CMAKE_VERSION VERSION_LESS 3.22)
set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.3.1.tar.gz")
set(_assimp_hash "SHA256=a07666be71afe1ad4bc008c2336b7c688aca391271188eb9108d0c6db1be53f1")
else()
set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz")
set(_assimp_hash "SHA256=66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb")
endif()
# Assimp's bundled zlib (contrib/zlib) is too old to compile against the modern
# macOS SDK: its zutil.h takes the classic-Mac branch under TARGET_OS_MAC and
# does `#define fdopen(fd,mode) NULL`, which then clobbers the SDK's real
# `fdopen` prototype in <stdio.h> and breaks the build. On macOS use the system
# zlib (already found by find_package(ZLIB) in deps-unix-common) instead.
if(APPLE)
set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=OFF")
else()
set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=ON")
endif()
orcaslicer_add_cmake_project(Assimp
URL ${_assimp_url}
URL_HASH ${_assimp_hash}
CMAKE_ARGS
-DASSIMP_BUILD_TESTS=OFF
-DASSIMP_BUILD_SAMPLES=OFF
-DASSIMP_BUILD_ASSIMP_TOOLS=OFF
-DASSIMP_INSTALL_PDB=OFF
-DASSIMP_NO_EXPORT=ON
-DASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT=OFF
-DASSIMP_BUILD_GLTF_IMPORTER=ON
-DASSIMP_BUILD_OBJ_IMPORTER=ON
-DASSIMP_BUILD_FBX_IMPORTER=ON
${_assimp_build_zlib}
-DASSIMP_WARNINGS_AS_ERRORS=OFF
-DBUILD_WITH_STATIC_CRT=OFF
)
if (MSVC)
add_debug_dep(dep_Assimp)
endif ()
+4
View File
@@ -367,6 +367,9 @@ include(libnoise/libnoise.cmake)
include(Draco/Draco.cmake)
# Assimp: glTF/GLB/FBX import for the texture-to-color feature.
include(Assimp/Assimp.cmake)
# I *think* 1.1 is used for *just* md5 hashing?
# 3.1 has everything in the right place, but the md5 funcs used are deprecated
@@ -448,6 +451,7 @@ set(_dep_list
dep_libnoise
dep_python3
dep_wxInspector
dep_Assimp
)
if (MSVC)
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4142,10 +4142,10 @@ msgid "PA Profile"
msgstr "Профіль PA"
msgid "Factor K"
msgstr "Коэф. K"
msgstr "Коеф. K"
msgid "Factor N"
msgstr "Коэф. N"
msgstr "Коеф. N"
msgid "Setting AMS slot information while printing is not supported"
msgstr "Зміна інформації про слоти AMS під час друку не підтримується"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Qidi",
"version": "02.04.00.10",
"version": "02.04.00.11",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
@@ -20,6 +20,9 @@
"close_fan_the_first_x_layers": [
"3"
],
"during_print_exhaust_fan_speed": [
"0"
],
"fan_cooling_layer_time": [
"10"
],
@@ -20,6 +20,9 @@
"close_fan_the_first_x_layers": [
"3"
],
"during_print_exhaust_fan_speed": [
"0"
],
"fan_cooling_layer_time": [
"10"
],
@@ -20,6 +20,9 @@
"close_fan_the_first_x_layers": [
"3"
],
"during_print_exhaust_fan_speed": [
"0"
],
"fan_cooling_layer_time": [
"10"
],
@@ -276,6 +276,12 @@ modules:
sha256: 27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
dest: external-packages/Draco
# Assimp 5.4.3
- type: file
url: https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz
sha256: 66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb
dest: external-packages/Assimp
# OpenSSL 1.1.1w (GNOME SDK has 3.x; OrcaSlicer requires 1.1.x)
- type: file
url: https://github.com/openssl/openssl/archive/OpenSSL_1_1_1w.tar.gz
+2
View File
@@ -3,7 +3,9 @@
#define _WIN32_WINNT 0x0502
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <wchar.h>
#include <commctrl.h>
+2
View File
@@ -2,7 +2,9 @@
#define _WIN32_WINNT 0x0502
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <shellapi.h>
#include <wchar.h>
+2 -2
View File
@@ -154,8 +154,8 @@ void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const
//h^2 = L^2 / b^2 [factor the divisor]
const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2);
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) //Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current, previous, next) <= scaled<double>(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) //Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current, previous, next) <= double(colinear_vertex_tolerance()))) // make sure that height_2 is not small because of cancellation of positive and negative areas
continue;
if (length2 < smallest_line_segment_squared
@@ -133,8 +133,8 @@ void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const
const auto height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2));
const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next);
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) // Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= scaled<double>(0.005)) // Make sure that height_2 is not small because of cancellation of positive and negative areas
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) // Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= double(colinear_vertex_tolerance())) // Make sure that height_2 is not small because of cancellation of positive and negative areas
// We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed
&& extrusion_area_error <= maximum_extrusion_area_deviation)
{
@@ -32,6 +32,14 @@ class Flow;
namespace Slic3r::Arachne
{
// ORCA: Tolerance of the "almost exactly colinear" early-out shared by the two simplify() passes
// (this file and WallToolPaths.cpp). That test drops a vertex regardless of the user's Maximum wall
// resolution/deviation, so it has to stay at the scale of coordinate rounding noise. A larger value
// silently decimates finely tessellated curves: on a circle, one vertex may be removed whenever the
// sagitta of the resulting chord falls below the tolerance, which halves the point count and turns
// smooth arcs into corners the firmware has to decelerate through.
inline coord_t colinear_vertex_tolerance() { return coord_t(SCALED_EPSILON); }
/*!
* Represents a polyline (not just a line) that is to be extruded with variable
* line width.
+17 -1
View File
@@ -179,6 +179,17 @@ set(lisbslic3r_sources
Fill/Lightning/Layer.hpp
Fill/Lightning/TreeNode.cpp
Fill/Lightning/TreeNode.hpp
FilamentMixer.cpp
FilamentMixer.hpp
FilamentMixerModel.hpp
ColorDecomposeRecipe.cpp
ColorDecomposeRecipe.hpp
TexturePainting.hpp
TexturePainting.cpp
TextureToColor/TextureToColor.hpp
TextureToColor/TextureToColor.cpp
TextureToColor/ColorUtils.hpp
TextureToColor/ColorUtils.cpp
Flow.cpp
Flow.hpp
FlushVolCalc.cpp
@@ -194,6 +205,9 @@ set(lisbslic3r_sources
format.hpp
Format/OBJ.cpp
Format/OBJ.hpp
Format/AssimpImport.hpp
Format/AssimpImport.cpp
Format/ResourcePathUtils.hpp
Format/objparser.cpp
Format/objparser.hpp
Format/SL1.cpp
@@ -511,6 +525,7 @@ cmake_policy(SET CMP0011 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0167 NEW)
find_package(CGAL REQUIRED)
find_package(OpenCV REQUIRED core)
find_package(assimp REQUIRED)
unset(CMAKE_POLICY_DEFAULT_CMP0167)
cmake_policy(POP)
@@ -551,7 +566,7 @@ target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTI
if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif()
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/TextureToColor PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS})
# Find the OCCT and related libraries
@@ -599,6 +614,7 @@ target_link_libraries(libslic3r
libnest2d
miniz
opencv_world
assimp::assimp
PRIVATE
${CMAKE_DL_LIBS}
${EXPAT_LIBRARIES}
+530
View File
@@ -0,0 +1,530 @@
#include "ColorDecomposeRecipe.hpp"
#include "FilamentMixer.hpp"
#include "Utils.hpp"
#include "nlohmann/json.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <limits>
#include <utility>
namespace Slic3r {
namespace {
struct LabColor {
double l{0.0};
double a{0.0};
double b{0.0};
};
struct StandardRecipeEntry {
ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::CMYW};
std::string material;
std::string source;
std::vector<std::string> component_keys;
std::vector<std::string> component_hexes;
std::vector<int> ratios;
std::string measured_hex;
LabColor measured_lab;
};
static double srgb_to_linear(double v)
{
v /= 255.0;
return v <= 0.04045 ? v / 12.92 : std::pow((v + 0.055) / 1.055, 2.4);
}
static double xyz_to_lab_component(double v)
{
constexpr double eps = 216.0 / 24389.0;
constexpr double kappa = 24389.0 / 27.0;
return v > eps ? std::cbrt(v) : (kappa * v + 16.0) / 116.0;
}
static LabColor rgb_to_lab(const ColorDecomposeRgb& rgb)
{
const double r = srgb_to_linear(rgb.r);
const double g = srgb_to_linear(rgb.g);
const double b = srgb_to_linear(rgb.b);
const double x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
const double y = (0.2126729 * r + 0.7151522 * g + 0.0721750 * b);
const double z = (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / 1.08883;
const double fx = xyz_to_lab_component(x);
const double fy = xyz_to_lab_component(y);
const double fz = xyz_to_lab_component(z);
return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)};
}
static std::string lab_to_srgb_hex(const LabColor& lab)
{
constexpr double Xn = 0.95047, Yn = 1.0, Zn = 1.08883;
auto f_inv = [](double t) -> double {
constexpr double eps = 216.0 / 24389.0;
constexpr double kappa = 24389.0 / 27.0;
const double t3 = t * t * t;
return t3 > eps ? t3 : (t * 116.0 - 16.0) / kappa;
};
const double fy = (lab.l + 16.0) / 116.0;
const double fx = lab.a / 500.0 + fy;
const double fz = fy - lab.b / 200.0;
const double X = Xn * f_inv(fx);
const double Y = Yn * f_inv(fy);
const double Z = Zn * f_inv(fz);
double r = 3.2406 * X - 1.5372 * Y - 0.4986 * Z;
double g = -0.9689 * X + 1.8758 * Y + 0.0415 * Z;
double b = 0.0557 * X - 0.2040 * Y + 1.0570 * Z;
auto gamma = [](double c) -> double {
c = std::max(0.0, std::min(1.0, c));
return c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055;
};
auto u8 = [&](double c) -> int {
return std::max(0, std::min(255, static_cast<int>(std::lround(gamma(c) * 255.0))));
};
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", u8(r), u8(g), u8(b));
return std::string(buf);
}
static double delta_e76(const LabColor& a, const LabColor& b)
{
return std::sqrt(std::pow(a.l - b.l, 2.0) + std::pow(a.a - b.a, 2.0) + std::pow(a.b - b.b, 2.0));
}
static bool material_matches(const std::string& a, const std::string& b)
{
if (a.empty() || b.empty())
return false;
return a == b || a == b + " Basic" || b == a + " Basic";
}
static std::vector<std::vector<int>> ratio_grid(size_t n)
{
std::vector<std::vector<int>> out;
if (n == 2) {
for (int a = 20; a <= 80; a += 5)
out.push_back({a, 100 - a});
} else if (n == 3) {
for (int a = 20; a <= 60; a += 5)
for (int b = 20; b <= 80 - a; b += 5) {
const int c = 100 - a - b;
if (c >= 20)
out.push_back({a, b, c});
}
}
return out;
}
static ColorDecomposeRecipeMode parse_mode(const std::string& s)
{
if (s == "RYBW" || s == "RGBY")
return ColorDecomposeRecipeMode::RYBW;
return ColorDecomposeRecipeMode::CMYW;
}
static std::vector<StandardRecipeEntry> load_standard_entries()
{
std::vector<StandardRecipeEntry> entries;
const std::string path = resources_dir() + "/filament_mixing/standard_color_recipes.json";
std::ifstream ifs(path);
if (!ifs)
return entries;
nlohmann::json root = nlohmann::json::parse(ifs, nullptr, false);
if (root.is_discarded() || !root.contains("entries") || !root["entries"].is_array())
return entries;
for (const auto& item : root["entries"]) {
if (!item.is_object())
continue;
StandardRecipeEntry entry;
entry.mode = parse_mode(item.value("mode", "CMYW"));
entry.material = item.value("material", "");
entry.source = item.value("source", "");
entry.measured_hex = item.value("measured_rgb", "");
if (item.contains("components") && item["components"].is_array()) {
for (const auto& comp : item["components"]) {
if (comp.is_object()) {
entry.component_keys.push_back(comp.value("key", ""));
entry.component_hexes.push_back(comp.value("rgb", ""));
}
}
}
if (item.contains("ratios") && item["ratios"].is_array()) {
for (const auto& ratio : item["ratios"]) {
if (ratio.is_number_integer())
entry.ratios.push_back(ratio.get<int>());
}
}
if (item.contains("measured_lab") && item["measured_lab"].is_array() && item["measured_lab"].size() >= 3) {
entry.measured_lab = {
item["measured_lab"][0].get<double>(),
item["measured_lab"][1].get<double>(),
item["measured_lab"][2].get<double>()
};
} else {
ColorDecomposeRgb measured_rgb;
if (!color_decompose_hex_to_rgb(entry.measured_hex, measured_rgb))
continue;
entry.measured_lab = rgb_to_lab(measured_rgb);
}
if (entry.component_hexes.size() >= 2 && entry.component_hexes.size() == entry.ratios.size() &&
!entry.measured_hex.empty())
entries.push_back(std::move(entry));
}
return entries;
}
static const std::vector<StandardRecipeEntry>& standard_entries()
{
static const std::vector<StandardRecipeEntry> entries = load_standard_entries();
return entries;
}
static void evaluate_candidate(const ColorDecomposeRgb& target,
const std::vector<std::string>& hexes,
const std::vector<int>& ratios,
const std::vector<unsigned int>& indices,
ColorDecomposeRecipeMode mode,
double& best_score,
ColorDecomposeRecipeResult& best)
{
const std::string mixed = blend_color_multi(hexes, ratios);
ColorDecomposeRgb mixed_rgb;
if (!color_decompose_hex_to_rgb(mixed, mixed_rgb))
return;
const double score = delta_e76(rgb_to_lab(target), rgb_to_lab(mixed_rgb));
if (score >= best_score)
return;
best_score = score;
best.valid = true;
best.mode = mode;
best.matched_color_hex = mixed;
best.components.clear();
for (size_t i = 0; i < hexes.size(); ++i) {
ColorDecomposeRecipeComponent comp;
comp.color_hex = hexes[i];
comp.ratio = ratios[i];
comp.filament_index = i < indices.size() ? indices[i] : 0;
best.components.push_back(comp);
}
}
} // namespace
std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb)
{
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
return std::string(buf);
}
bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out)
{
if (hex.size() < 7 || hex[0] != '#')
return false;
unsigned r = 0, g = 0, b = 0;
if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &r, &g, &b) != 3)
return false;
out = {static_cast<unsigned char>(r), static_cast<unsigned char>(g), static_cast<unsigned char>(b)};
return true;
}
ColorDecomposeRecipeResult recommend_from_physical_filaments(
const ColorDecomposeRgb& target,
const std::vector<ColorDecomposePhysicalFilament>& physical_filaments,
const std::string& preferred_material_type)
{
std::vector<ColorDecomposePhysicalFilament> candidates;
for (const auto& filament : physical_filaments) {
if (filament.is_mixed)
continue;
ColorDecomposeRgb ignored;
if (!color_decompose_hex_to_rgb(filament.color_hex, ignored))
continue;
if (preferred_material_type.empty() || material_matches(filament.type, preferred_material_type))
candidates.push_back(filament);
}
// Early exit: if a material-matched candidate has the exact target color,
// return it as 100%. Downstream rejects single-component results (no mixed
// slot created), which is correct -- the color already exists.
const std::string target_hex = color_decompose_rgb_to_hex(target);
for (const auto& cand : candidates) {
ColorDecomposeRgb cand_rgb;
if (!color_decompose_hex_to_rgb(cand.color_hex, cand_rgb))
continue;
if (color_decompose_rgb_to_hex(cand_rgb) == target_hex) {
ColorDecomposeRecipeResult exact;
exact.valid = true;
exact.mode = ColorDecomposeRecipeMode::MaterialList;
exact.matched_color_hex = cand.color_hex;
ColorDecomposeRecipeComponent comp;
comp.color_hex = cand.color_hex;
comp.ratio = 100;
comp.filament_index = cand.filament_index;
exact.components.push_back(comp);
return exact;
}
}
if (candidates.size() < 2)
candidates = physical_filaments;
candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [](const auto& filament) {
if (filament.is_mixed)
return true;
ColorDecomposeRgb ignored;
return !color_decompose_hex_to_rgb(filament.color_hex, ignored);
}), candidates.end());
constexpr size_t kMaxCandidates = 8;
if (candidates.size() > kMaxCandidates) {
const LabColor target_lab = rgb_to_lab(target);
std::sort(candidates.begin(), candidates.end(),
[&target_lab](const ColorDecomposePhysicalFilament& a, const ColorDecomposePhysicalFilament& b) {
ColorDecomposeRgb rgb_a, rgb_b;
color_decompose_hex_to_rgb(a.color_hex, rgb_a);
color_decompose_hex_to_rgb(b.color_hex, rgb_b);
return delta_e76(target_lab, rgb_to_lab(rgb_a))
< delta_e76(target_lab, rgb_to_lab(rgb_b));
});
candidates.resize(kMaxCandidates);
}
ColorDecomposeRecipeResult best;
double best_score = std::numeric_limits<double>::max();
for (size_t i = 0; i < candidates.size(); ++i) {
for (size_t j = i + 1; j < candidates.size(); ++j) {
const std::vector<std::string> hexes = {candidates[i].color_hex, candidates[j].color_hex};
const std::vector<unsigned int> indices = {candidates[i].filament_index, candidates[j].filament_index};
for (const auto& ratios : ratio_grid(2))
evaluate_candidate(target, hexes, ratios, indices, ColorDecomposeRecipeMode::MaterialList, best_score, best);
for (size_t k = j + 1; k < candidates.size(); ++k) {
const std::vector<std::string> hexes3 = {candidates[i].color_hex, candidates[j].color_hex, candidates[k].color_hex};
const std::vector<unsigned int> indices3 = {candidates[i].filament_index, candidates[j].filament_index, candidates[k].filament_index};
for (const auto& ratios : ratio_grid(3))
evaluate_candidate(target, hexes3, ratios, indices3, ColorDecomposeRecipeMode::MaterialList, best_score, best);
}
}
}
return best;
}
ColorDecomposeRecipeResult lookup_standard_recipe(
const ColorDecomposeRgb& target,
ColorDecomposeRecipeMode mode,
const std::string& preferred_material_type)
{
const LabColor target_lab = rgb_to_lab(target);
ColorDecomposeRecipeResult best;
double best_score = std::numeric_limits<double>::max();
auto consider = [&](bool require_material_match) {
for (const StandardRecipeEntry& entry : standard_entries()) {
if (entry.mode != mode)
continue;
if (require_material_match && !material_matches(entry.material, preferred_material_type))
continue;
if (!require_material_match && !preferred_material_type.empty() && material_matches(entry.material, preferred_material_type))
continue;
const double score = delta_e76(target_lab, entry.measured_lab);
if (score >= best_score)
continue;
best_score = score;
best.valid = true;
best.mode = mode;
best.matched_color_hex = entry.measured_hex;
best.components.clear();
for (size_t i = 0; i < entry.component_hexes.size(); ++i) {
ColorDecomposeRecipeComponent comp;
comp.color_hex = entry.component_hexes[i];
comp.base_color = i < entry.component_keys.size() ? entry.component_keys[i] : "";
comp.ratio = entry.ratios[i];
comp.filament_index = 0;
best.components.push_back(comp);
}
}
};
consider(true);
if (!best.valid)
consider(false);
return best;
}
std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes,
const std::vector<int>& ratios)
{
if (component_hexes.size() < 2 || component_hexes.size() != ratios.size())
return {};
auto normalize_hex = [](const std::string& hex) -> std::string {
ColorDecomposeRgb rgb;
if (!color_decompose_hex_to_rgb(hex, rgb))
return {};
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
return std::string(buf);
};
// Stage 1: canonicalize input by sorting (hex, ratio) pairs so matching
// is independent of the caller's component order.
const size_t n = component_hexes.size();
std::vector<std::pair<std::string, int>> in_pairs;
in_pairs.reserve(n);
for (size_t i = 0; i < n; ++i) {
std::string nh = normalize_hex(component_hexes[i]);
if (nh.empty())
return {};
in_pairs.emplace_back(std::move(nh), ratios[i]);
}
std::sort(in_pairs.begin(), in_pairs.end());
std::vector<std::string> in_hexes;
std::vector<int> in_ratios;
in_hexes.reserve(n);
in_ratios.reserve(n);
for (const auto& p : in_pairs) {
in_hexes.push_back(p.first);
in_ratios.push_back(p.second);
}
// Normalize ratios to sum=100 (callers may pass arbitrary weights,
// e.g. MixedFilamentDialog uses ratio*10000).
{
int sum = 0;
for (int r : in_ratios) sum += r;
if (sum > 0 && sum != 100) {
int new_sum = 0;
for (size_t i = 0; i < in_ratios.size(); ++i) {
in_ratios[i] = static_cast<int>(std::lround(
static_cast<double>(in_ratios[i]) * 100.0 / static_cast<double>(sum)));
new_sum += in_ratios[i];
}
if (new_sum != 100) {
auto it = std::max_element(in_ratios.begin(), in_ratios.end());
*it += (100 - new_sum);
}
}
}
// Fall back to polynomial model for ratios outside the measured range.
{
bool out_of_range = false;
if (n == 2) {
for (int r : in_ratios)
if (r < 20 || r > 80) { out_of_range = true; break; }
} else {
for (int r : in_ratios)
if (r < 20) { out_of_range = true; break; }
}
if (out_of_range)
return {};
}
// Stage 2: collect anchors with the same component hex set; try exact match.
struct Anchor {
std::vector<int> ratios;
LabColor lab;
std::string hex;
};
std::vector<Anchor> anchors;
for (const StandardRecipeEntry& entry : standard_entries()) {
if (entry.source != "measured" && entry.source != "interpolated")
continue;
if (entry.component_hexes.size() != n)
continue;
std::vector<std::pair<std::string, int>> e_pairs;
e_pairs.reserve(n);
for (size_t i = 0; i < n; ++i)
e_pairs.emplace_back(normalize_hex(entry.component_hexes[i]), entry.ratios[i]);
std::sort(e_pairs.begin(), e_pairs.end());
bool same_set = true;
for (size_t i = 0; i < n; ++i)
if (e_pairs[i].first != in_hexes[i]) { same_set = false; break; }
if (!same_set)
continue;
Anchor a;
a.ratios.reserve(n);
for (const auto& p : e_pairs) a.ratios.push_back(p.second);
a.lab = entry.measured_lab;
a.hex = entry.measured_hex;
if (a.ratios == in_ratios)
return a.hex;
anchors.push_back(std::move(a));
}
if (anchors.size() < 2)
return {};
// Stage 3: interpolation in Lab space.
if (n == 2) {
// 1D linear interpolation along ratio[0].
std::sort(anchors.begin(), anchors.end(),
[](const Anchor& a, const Anchor& b) { return a.ratios[0] < b.ratios[0]; });
const double x = static_cast<double>(in_ratios[0]);
size_t lo = 0;
while (lo + 2 < anchors.size() && static_cast<double>(anchors[lo + 1].ratios[0]) <= x)
++lo;
const Anchor& a0 = anchors[lo];
const Anchor& a1 = anchors[lo + 1];
const double span = static_cast<double>(a1.ratios[0] - a0.ratios[0]);
const double t = span > 0.0 ? (x - static_cast<double>(a0.ratios[0])) / span : 0.0;
return lab_to_srgb_hex({a0.lab.l + t * (a1.lab.l - a0.lab.l),
a0.lab.a + t * (a1.lab.a - a0.lab.a),
a0.lab.b + t * (a1.lab.b - a0.lab.b)});
}
// 3+ color: IDW (p=2) with 3 nearest anchors in the (ratio[0], ratio[1]) plane.
const double ra = static_cast<double>(in_ratios[0]);
const double rb = static_cast<double>(in_ratios[1]);
std::vector<std::pair<double, const Anchor*>> dists;
dists.reserve(anchors.size());
for (const Anchor& a : anchors) {
const double d = std::sqrt(std::pow(ra - static_cast<double>(a.ratios[0]), 2.0) +
std::pow(rb - static_cast<double>(a.ratios[1]), 2.0));
if (d == 0.0)
return a.hex;
dists.emplace_back(d, &a);
}
const size_t k = std::min(static_cast<size_t>(3), dists.size());
std::partial_sort(dists.begin(), dists.begin() + k, dists.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
double num_l = 0.0, num_a = 0.0, num_b = 0.0, den = 0.0;
for (size_t j = 0; j < k; ++j) {
const double w = 1.0 / (dists[j].first * dists[j].first);
num_l += w * dists[j].second->lab.l;
num_a += w * dists[j].second->lab.a;
num_b += w * dists[j].second->lab.b;
den += w;
}
return lab_to_srgb_hex({num_l / den, num_a / den, num_b / den});
}
} // namespace Slic3r
+64
View File
@@ -0,0 +1,64 @@
#ifndef SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP
#define SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP
#include <string>
#include <vector>
namespace Slic3r {
enum class ColorDecomposeRecipeMode {
MaterialList,
CMYW,
RYBW
};
struct ColorDecomposeRgb {
unsigned char r{0};
unsigned char g{0};
unsigned char b{0};
};
struct ColorDecomposePhysicalFilament {
std::string color_hex;
std::string name;
std::string type;
bool is_mixed{false};
unsigned int filament_index{0}; // 1-based physical filament index
};
struct ColorDecomposeRecipeComponent {
std::string color_hex;
std::string base_color;
int ratio{0};
unsigned int filament_index{0}; // 1-based for physical filaments, 0 for standard base colors
};
struct ColorDecomposeRecipeResult {
bool valid{false};
ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::MaterialList};
std::string matched_color_hex;
std::vector<ColorDecomposeRecipeComponent> components;
};
std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb);
bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out);
ColorDecomposeRecipeResult recommend_from_physical_filaments(
const ColorDecomposeRgb& target,
const std::vector<ColorDecomposePhysicalFilament>& physical_filaments,
const std::string& preferred_material_type);
ColorDecomposeRecipeResult lookup_standard_recipe(
const ColorDecomposeRgb& target,
ColorDecomposeRecipeMode mode,
const std::string& preferred_material_type);
// Look up the measured blend color for an exact (component_hexes, ratios) match
// in the standard color recipe table. Returns the measured hex color if found
// with reliable source data ("measured" or "interpolated"), empty string otherwise.
std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes,
const std::vector<int>& ratios);
} // namespace Slic3r
#endif // SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP
+2 -1
View File
@@ -2031,7 +2031,8 @@ const double& DynamicConfig::opt_float(const t_config_option_key &opt_key, unsig
return opt_floats_nullable->get_at(idx);
} else {
assert(false);
return 0;
static const double zero = 0.0;
return zero;
}
}
+14 -5
View File
@@ -682,7 +682,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
return fuzzified;
}
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour)
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed)
{
const auto slice_z = perimeter_generator.slice_z;
const auto& regions = perimeter_generator.regions_by_fuzzify;
@@ -690,7 +690,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
if (fuzzify)
fuzzy_extrusion_line(extrusion->junctions, slice_z, config);
fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed);
} else {
// Merge regions that produce identical fuzzy effects (differ only in type).
// When the style (e.g. External) and a painted region (All) both fuzzify this loop
@@ -701,10 +701,19 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
// Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config);
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed);
return;
}
// Open path means this is a thin wall that collapsed into a single thick line, in this case the path will go exactly
// between the middle two sides of the object. And since the paint segmentation never goes beyond the middle line because
// it uses voronoi diagram, we need to expand the segmentation a little bit to make sure it covers the path.
if (!closed) {
for (auto& r : merged_regions) {
r.expolygons = offset_ex(r.expolygons, perimeter_generator.ext_perimeter_flow.scaled_width() / 10);
}
}
#ifdef DEBUG_FUZZY
{
int i = 0;
@@ -752,7 +761,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
// Fuzzy splitted extrusion
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
// The entire polygon is fuzzified
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config);
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed);
continue;
} else {
const auto current_ext = extrusion->junctions;
@@ -803,7 +812,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
}
//Orca: ensure the loop is closed after fuzzy
if (!extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
if (closed && !extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
extrusion->junctions.back().p = extrusion->junctions.front().p;
extrusion->junctions.back().w = extrusion->junctions.front().w;
}
@@ -16,7 +16,7 @@ void group_region_by_fuzzify(PerimeterGenerator& g);
bool should_fuzzify(const FuzzySkinConfig& config, int layer_id, size_t loop_idx, bool is_contour);
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, size_t loop_idx, bool is_contour);
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour);
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour, bool closed = true);
} // namespace Slic3r::Feature::FuzzySkin
+1 -1
View File
@@ -1021,7 +1021,7 @@ namespace Slic3r
if (FGMode::MatchMode == ctx.group_info.mode)
return calc_filament_group_for_match(cost);
}
catch (const FilamentGroupException& e) {
catch (const FilamentGroupException&) {
}
return calc_filament_group_for_flush(cost);
+829
View File
@@ -0,0 +1,829 @@
#include "FilamentMixer.hpp"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <limits>
#include <set>
#include <sstream>
#include <numeric>
#include <boost/log/trivial.hpp>
#include "ColorDecomposeRecipe.hpp"
#include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp"
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<unsigned char>(clamped * 255.0f + 0.5f);
}
inline float to_f01(unsigned char x)
{
return static_cast<float>(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));
}
static bool parse_hex(const std::string &hex, unsigned char &r, unsigned char &g, unsigned char &b)
{
if (hex.size() < 7 || hex[0] != '#') return false;
unsigned rv = 0, gv = 0, bv = 0;
if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &rv, &gv, &bv) != 3) return false;
r = (unsigned char)rv; g = (unsigned char)gv; b = (unsigned char)bv;
return true;
}
std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b)
{
unsigned char r1 = 128, g1 = 128, b1 = 128;
unsigned char r2 = 128, g2 = 128, b2 = 128;
parse_hex(hex_a, r1, g1, b1);
parse_hex(hex_b, r2, g2, b2);
unsigned char mr = 0, mg = 0, mb = 0;
filament_mixer_lerp(r1, g1, b1, r2, g2, b2, ratio_b, &mr, &mg, &mb);
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", mr, mg, mb);
return std::string(buf);
}
std::string blend_color_multi(const std::vector<std::string> &hex_colors,
const std::vector<int> &weights)
{
if (hex_colors.size() >= 2 && hex_colors.size() == weights.size()) {
std::string measured = lookup_measured_blend_color(hex_colors, weights);
if (!measured.empty())
return measured;
}
if (hex_colors.empty())
return "#000000";
if (hex_colors.size() == 1) {
unsigned char cr = 128, cg = 128, cb = 128;
parse_hex(hex_colors.front(), cr, cg, cb);
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", cr, cg, cb);
return std::string(buf);
}
assert(hex_colors.size() == weights.size());
unsigned char r = 128, g = 128, b = 128;
int accumulated = 0;
for (size_t i = 0; i < hex_colors.size() && i < weights.size(); ++i) {
if (weights[i] <= 0)
continue;
unsigned char cr = 128, cg = 128, cb = 128;
parse_hex(hex_colors[i], cr, cg, cb);
if (accumulated == 0) {
r = cr; g = cg; b = cb;
accumulated = weights[i];
} else {
const int new_total = accumulated + weights[i];
const float t = static_cast<float>(weights[i]) / static_cast<float>(new_total);
filament_mixer_lerp(r, g, b, cr, cg, cb, t, &r, &g, &b);
accumulated = new_total;
}
}
if (accumulated == 0)
return "#000000";
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b);
return std::string(buf);
}
std::vector<unsigned int> parse_mixed_components(const std::string &str)
{
std::vector<unsigned int> components;
if (str.empty())
return components;
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
try {
int val = std::stoi(token);
if (val >= 0)
components.push_back(static_cast<unsigned int>(val));
} catch (...) {}
}
return components;
}
namespace {
// Parse a token that may represent a finite double or "use default" (empty / "nan").
// Returns NaN on either explicit sentinel or any parse error.
inline double parse_tangent_token(const std::string& tok)
{
if (tok.empty()) return std::numeric_limits<double>::quiet_NaN();
std::string lower(tok.size(), '\0');
std::transform(tok.begin(), tok.end(), lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lower == "nan") return std::numeric_limits<double>::quiet_NaN();
try {
const double v = std::stod(tok);
if (!std::isfinite(v)) return std::numeric_limits<double>::quiet_NaN();
return v;
} catch (...) {
return std::numeric_limits<double>::quiet_NaN();
}
}
// Split a "a,b,c,d" segment on commas, preserving empty tokens (so "0.5,0.4,," yields
// {"0.5","0.4","",""}). Used by the gradient-curve parser to distinguish NaN tangents
// from a malformed segment.
inline std::vector<std::string> split_commas(const std::string& seg)
{
std::vector<std::string> out;
size_t start = 0;
while (true) {
const size_t comma = seg.find(',', start);
if (comma == std::string::npos) {
out.emplace_back(seg.substr(start));
return out;
}
out.emplace_back(seg.substr(start, comma - start));
start = comma + 1;
}
}
} // namespace
// Default Fritsch-Carlson PCHIP tangents for a sorted-by-x anchor list. m has size n
// matching the anchor count; for n == 1 the tangent is 0; for n == 2 both endpoint
// tangents equal the single secant (degenerates to linear).
std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts)
{
const size_t n = pts.size();
std::vector<double> m(n, 0.0);
if (n < 2) return m;
std::vector<double> d(n - 1);
for (size_t i = 0; i + 1 < n; ++i) {
const double h = std::max(1e-12, pts[i + 1].x - pts[i].x);
d[i] = (pts[i + 1].y - pts[i].y) / h;
}
m[0] = d[0];
m[n - 1] = d[n - 2];
for (size_t i = 1; i + 1 < n; ++i)
m[i] = 0.5 * (d[i - 1] + d[i]);
// Fritsch-Carlson monotonic guard: kill flats then rescale steep tangents so the
// resulting cubic never overshoots [min, max] of the surrounding anchors.
for (size_t i = 0; i + 1 < n; ++i) {
if (d[i] == 0.0) {
m[i] = 0.0;
m[i + 1] = 0.0;
continue;
}
const double a = m[i] / d[i];
const double b = m[i + 1] / d[i];
const double s = a * a + b * b;
if (s > 9.0) {
const double tau = 3.0 / std::sqrt(s);
m[i] = tau * a * d[i];
m[i + 1] = tau * b * d[i];
}
}
return m;
}
GradientCurve parse_gradient_curve(const std::string& s)
{
GradientCurve curve;
if (s.empty())
return curve;
CNumericLocalesSetter c_locale_setter;
std::istringstream ss(s);
std::string segment;
while (std::getline(ss, segment, '|')) {
if (segment.empty())
continue;
const auto fields = split_commas(segment);
// 2-field legacy form -> (x, y), tangents stay NaN.
// 4-field form -> (x, y, m_in, m_out), empty / "nan" tokens preserved as NaN.
if (fields.size() != 2 && fields.size() != 4) {
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring malformed segment \""
<< segment << "\" (expected 2 or 4 comma-separated fields, got "
<< fields.size() << ")";
continue;
}
try {
double x = std::stod(fields[0]);
double y = std::stod(fields[1]);
x = std::max(0.0, std::min(1.0, x));
y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, y));
GradientAnchor a;
a.x = x;
a.y = y;
if (fields.size() == 4) {
a.m_in = parse_tangent_token(fields[2]);
a.m_out = parse_tangent_token(fields[3]);
}
curve.points.push_back(a);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring unparseable segment \""
<< segment << "\": " << e.what();
}
}
if (curve.points.size() < 2) {
if (!curve.points.empty())
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: only "
<< curve.points.size() << " valid point(s), need at least 2; discarding";
curve.points.clear();
return curve;
}
std::sort(curve.points.begin(), curve.points.end(),
[](const GradientAnchor& a, const GradientAnchor& b) {
return a.x < b.x;
});
return curve;
}
std::string serialize_gradient_curve(const GradientCurve& c)
{
if (c.points.empty())
return std::string{};
CNumericLocalesSetter c_locale_setter;
std::string out;
char buf[128];
for (size_t i = 0; i < c.points.size(); ++i) {
if (i > 0) out += '|';
const auto& a = c.points[i];
const bool has_in = std::isfinite(a.m_in);
const bool has_out = std::isfinite(a.m_out);
if (has_in || has_out) {
// Emit empty tokens for NaN slots so the legacy parser would still split
// four fields; the new parser interprets empty tokens as "use PCHIP default".
char in_buf[32] = {0};
char out_buf[32] = {0};
if (has_in) std::snprintf(in_buf, sizeof(in_buf), "%.4f", a.m_in);
if (has_out) std::snprintf(out_buf, sizeof(out_buf), "%.4f", a.m_out);
std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%s,%s",
a.x, a.y, in_buf, out_buf);
} else {
// 4-field form is only emitted when at least one tangent is finite; the
// 2-field form is emitted otherwise so the JSON payload stays minimal
// and remains readable by older clients that only know (x, y) pairs.
std::snprintf(buf, sizeof(buf), "%.4f,%.4f", a.x, a.y);
}
out += buf;
}
return out;
}
double sample_gradient_curve(const GradientCurve& c, double t)
{
const auto& pts = c.points;
if (pts.size() < 2)
return 0.5;
if (t <= pts.front().x)
return pts.front().y;
if (t >= pts.back().x)
return pts.back().y;
// PCHIP defaults are computed for every call; control point counts are typically
// tiny (< 16) so the allocation cost is negligible compared to any actual rendering
// or G-code work that drives the sampler.
const std::vector<double> m_def = compute_pchip_default_tangents(pts);
const size_t n = pts.size();
// Linear scan to locate the interval [pts[i].x, pts[i+1].x] containing t. Cheap
// and avoids the upper_bound boilerplate; n is small.
for (size_t i = 1; i < n; ++i) {
const double x0 = pts[i - 1].x;
const double x1 = pts[i].x;
if (t > x1) continue;
const double y0 = pts[i - 1].y;
const double y1 = pts[i].y;
const double h = std::max(1e-12, x1 - x0);
const double m_left = std::isfinite(pts[i - 1].m_out) ? pts[i - 1].m_out : m_def[i - 1];
const double m_right = std::isfinite(pts[i].m_in) ? pts[i].m_in : m_def[i];
const double u = (t - x0) / h;
const double u2 = u * u;
const double u3 = u2 * u;
const double h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
const double h10 = u3 - 2.0 * u2 + u;
const double h01 = -2.0 * u3 + 3.0 * u2;
const double h11 = u3 - u2;
double y = h00 * y0 + h10 * h * m_left
+ h01 * y1 + h11 * h * m_right;
// Defensive clamp in case tangent overrides on legacy curves push the
// single-segment Hermite slightly outside the anchor band.
if (y < kGradientMinRatio) y = kGradientMinRatio;
if (y > kGradientMaxRatio) y = kGradientMaxRatio;
return y;
}
return pts.back().y;
}
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components)
{
CNumericLocalesSetter c_locale_setter;
std::vector<double> ratios;
if (!str.empty()) {
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
try {
double val = std::stod(token);
if (val > 0.0)
ratios.push_back(val);
} catch (...) {}
}
}
if (ratios.size() != n_components || n_components == 0) {
ratios.assign(n_components, n_components > 0 ? 1.0 / n_components : 0.0);
return ratios;
}
double sum = std::accumulate(ratios.begin(), ratios.end(), 0.0);
if (sum > 0.0 && std::abs(sum - 1.0) > 1e-6) {
for (double &r : ratios)
r /= sum;
}
return ratios;
}
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
{
for (unsigned char v : is_mixed)
if (v) return true;
return false;
}
std::vector<size_t> check_mixed_filament_integrity(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
size_t num_physical)
{
std::vector<size_t> broken;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i]) continue;
if (i >= comp_strs.size() || comp_strs[i].empty()) {
broken.push_back(i);
continue;
}
auto comps = parse_mixed_components(comp_strs[i]);
if (comps.size() < 2) {
broken.push_back(i);
continue;
}
for (unsigned int c : comps) {
if (c < 1 || c > num_physical) {
broken.push_back(i);
break;
}
}
}
return broken;
}
std::vector<unsigned int> expand_mixed_filaments(
const std::vector<unsigned int> &extruders_0based,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs)
{
std::vector<unsigned int> result;
for (unsigned int ext : extruders_0based) {
if (ext < is_mixed.size() && is_mixed[ext] && ext < comp_strs.size()) {
auto comps = parse_mixed_components(comp_strs[ext]);
for (unsigned int c : comps)
if (c >= 1) result.push_back(c - 1);
} else {
result.push_back(ext);
}
}
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
void remap_mixed_components_on_delete(
const std::vector<unsigned char> &is_mixed,
std::vector<std::string> &comp_strs,
unsigned int del_1based)
{
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i]) continue;
if (i >= comp_strs.size() || comp_strs[i].empty()) continue;
auto comps = parse_mixed_components(comp_strs[i]);
std::ostringstream ss;
for (size_t j = 0; j < comps.size(); ++j) {
if (j > 0) ss << ',';
if (comps[j] == del_1based)
ss << 0;
else if (comps[j] > del_1based)
ss << (comps[j] - 1);
else
ss << comps[j];
}
comp_strs[i] = ss.str();
}
}
std::vector<size_t> check_mixed_filament_type_consistency(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &filament_types)
{
std::vector<size_t> result;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i]) continue;
if (i >= comp_strs.size() || comp_strs[i].empty()) continue;
auto comps = parse_mixed_components(comp_strs[i]);
if (comps.size() < 2) continue;
std::string ref_type;
bool mismatch = false;
for (unsigned int c : comps) {
if (c == 0) continue; // sentinel for deleted component
size_t idx = static_cast<size_t>(c) - 1; // 1-based -> 0-based
if (idx >= filament_types.size()) continue;
if (ref_type.empty())
ref_type = filament_types[idx];
else if (filament_types[idx] != ref_type) {
mismatch = true;
break;
}
}
if (mismatch)
result.push_back(i);
}
return result;
}
void expand_mixed_slots_in_unprintables(
std::vector<std::set<int>> &unprintables,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs)
{
for (auto &unprintable_set : unprintables) {
std::set<int> expanded;
for (int fid : unprintable_set) {
if (fid >= 0 && (size_t)fid < is_mixed.size() && is_mixed[fid]
&& (size_t)fid < comp_strs.size()) {
auto comps = parse_mixed_components(comp_strs[fid]);
for (unsigned int c : comps)
if (c >= 1) expanded.insert((int)(c - 1));
} else {
expanded.insert(fid);
}
}
unprintable_set = std::move(expanded);
}
}
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals)
{
for (size_t i = 0; i < vals.size(); ++i) {
if (vals[i].empty())
continue;
// parse_gradient_curve returns empty for both "empty input" and "<2 valid points";
// we already skipped empty, so an empty result means a corrupted single-point slot.
if (parse_gradient_curve(vals[i]).empty()) {
BOOST_LOG_TRIVIAL(warning) << "sanitize_mixed_gradient_curve_array: slot "
<< i << " curve \"" << vals[i]
<< "\" has fewer than 2 valid points; clearing to linear";
vals[i].clear();
}
}
}
bool try_parse_mixed_components_strict(const std::string &str,
std::vector<unsigned int> &components,
std::string &err)
{
components.clear();
if (str.empty()) {
err = "empty component list";
return false;
}
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty component index";
return false;
}
try {
const long val = std::stol(token);
if (val < 1) {
err = "component index must be >= 1 (got " + token + ")";
return false;
}
components.push_back(static_cast<unsigned int>(val));
} catch (...) {
err = "invalid component index \"" + token + "\"";
return false;
}
}
if (components.size() < 2) {
err = "at least 2 components required (got " + std::to_string(components.size()) + ")";
return false;
}
std::set<unsigned int> seen;
for (unsigned int c : components) {
if (!seen.insert(c).second) {
err = "duplicate component index " + std::to_string(c);
return false;
}
}
return true;
}
bool try_parse_mixed_ratios_strict(const std::string &str,
size_t n_components,
std::string &err)
{
if (str.empty())
return true;
CNumericLocalesSetter c_locale_setter;
std::vector<double> ratios;
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty ratio value";
return false;
}
try {
const double val = std::stod(token);
if (!(val > 0.0)) {
err = "ratio must be positive (got " + token + ")";
return false;
}
ratios.push_back(val);
} catch (...) {
err = "invalid ratio \"" + token + "\"";
return false;
}
}
if (ratios.size() != n_components) {
err = "expected " + std::to_string(n_components) + " ratio(s), got "
+ std::to_string(ratios.size());
return false;
}
return true;
}
bool validate_gradient_range_strict(const std::string &str, std::string &err)
{
if (str.empty())
return true;
CNumericLocalesSetter c_locale_setter;
float v0 = 0.f, v1 = 0.f;
if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) {
err = "expected two comma-separated floats, e.g. \"0.10,0.90\"";
return false;
}
if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) {
err = "start and end ratios must be in (0, 1)";
return false;
}
return true;
}
static void append_error(std::map<std::string, std::string> &errors,
const std::string &key,
const std::string &msg)
{
auto it = errors.find(key);
if (it == errors.end())
errors.emplace(key, msg);
else
it->second += "; " + msg;
}
static bool has_mixed_sub_params_specified(
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags)
{
for (const std::string &s : comp_strs)
if (!s.empty()) return true;
for (const std::string &s : ratio_strs)
if (!s.empty()) return true;
for (unsigned char g : gradient_flags)
if (g) return true;
return false;
}
static bool mixed_string_array_was_specified(const std::vector<std::string> &vals)
{
for (const std::string &s : vals)
if (!s.empty())
return true;
return false;
}
static bool mixed_bool_array_was_specified(const std::vector<unsigned char> &vals)
{
for (unsigned char v : vals)
if (v)
return true;
return false;
}
static void check_mixed_array_size_required(std::map<std::string, std::string> &errors,
const std::string &opt_key,
size_t actual_size,
size_t expected_size)
{
if (actual_size != expected_size) {
append_error(errors, opt_key,
"array size " + std::to_string(actual_size)
+ " does not match filament slot count " + std::to_string(expected_size));
}
}
std::map<std::string, std::string> validate_mixed_filament_params(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags,
const std::vector<std::string> &gradient_range_strs,
const std::vector<std::string> &gradient_curve_strs)
{
std::map<std::string, std::string> errors;
if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags)
&& !has_any_mixed_filament(is_mixed)) {
append_error(errors, "filament_is_mixed",
"must be set when mixed filament parameters are specified");
return errors;
}
if (!has_any_mixed_filament(is_mixed))
return errors;
const size_t slot_count = is_mixed.size();
// Rule 1: mixed filament model → components & ratios arrays must cover every slot.
check_mixed_array_size_required(errors, "filament_mixed_components", comp_strs.size(), slot_count);
check_mixed_array_size_required(errors, "filament_mixed_sublayer_ratios", ratio_strs.size(), slot_count);
// Rule 2: gradient passed (any slot true) → gradient & range arrays must cover every slot.
const bool gradient_specified = mixed_bool_array_was_specified(gradient_flags);
if (gradient_specified) {
check_mixed_array_size_required(errors, "filament_mixed_gradient", gradient_flags.size(), slot_count);
check_mixed_array_size_required(errors, "filament_mixed_gradient_range", gradient_range_strs.size(), slot_count);
}
// Rule 3: curve passed (any non-empty entry) → curve array must cover every slot.
const bool curve_specified = mixed_string_array_was_specified(gradient_curve_strs);
if (curve_specified)
check_mixed_array_size_required(errors, "filament_mixed_gradient_curve", gradient_curve_strs.size(), slot_count);
size_t num_physical = 0;
for (unsigned char v : is_mixed)
if (!v) ++num_physical;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
const std::string slot = "slot " + std::to_string(i + 1);
const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : "";
std::vector<unsigned int> components;
std::string comp_err;
if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) {
append_error(errors, "filament_mixed_components", slot + ": " + comp_err);
continue;
}
for (unsigned int c : components) {
if (c > num_physical) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " out of range (max physical filament index is "
+ std::to_string(num_physical) + ")");
break;
}
if (c == i + 1) {
append_error(errors, "filament_mixed_components",
slot + ": cannot reference itself as a component");
break;
}
const size_t idx0 = static_cast<size_t>(c - 1);
if (idx0 < is_mixed.size() && is_mixed[idx0]) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " references a mixed filament slot");
break;
}
}
std::string ratio_err;
const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : "";
if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err))
append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err);
const bool gradient_on = i < gradient_flags.size() && gradient_flags[i];
if (gradient_on) {
if (components.size() != 2) {
append_error(errors, "filament_mixed_gradient",
slot + ": gradient requires exactly 2 components");
}
if (gradient_specified) {
std::string range_err;
const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : "";
if (!validate_gradient_range_strict(range_str, range_err))
append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err);
}
if (curve_specified) {
const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : "";
if (!curve_str.empty() && parse_gradient_curve(curve_str).empty())
append_error(errors, "filament_mixed_gradient_curve",
slot + ": invalid curve (need at least 2 valid control points)");
}
}
}
return errors;
}
} // namespace Slic3r
+164
View File
@@ -0,0 +1,164 @@
#ifndef SLIC3R_FILAMENT_MIXER_HPP
#define SLIC3R_FILAMENT_MIXER_HPP
#include <limits>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
// Photoshop-style gradient curve control point in [0,1] x [0,1].
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
// anchors loaded from old 2-field 3MF projects or freshly added via a quick click.
// A press-and-drag on a curve segment populates m_out of its left anchor and m_in of
// its right anchor so the segment bends without inserting a new anchor.
struct GradientAnchor {
double x = 0.0;
double y = 0.0;
double m_in = std::numeric_limits<double>::quiet_NaN();
double m_out = std::numeric_limits<double>::quiet_NaN();
};
// Sorted list of GradientAnchor; x in [0,1], y in [kGradientMinRatio, kGradientMaxRatio].
// Empty means "no custom curve" (callers should fall back to the linear range).
struct GradientCurve {
std::vector<GradientAnchor> points;
bool empty() const { return points.empty(); }
};
// Reserved blend ratio range. Anchor y values (= component 0's ratio) are constrained
// to this band so the mixed filament never reaches pure 0% / 100% of either physical
// component, which keeps both extruders flowing and avoids degenerate transitions.
// Both the editor and the sampler enforce this clamp.
constexpr double kGradientMinRatio = 0.1;
constexpr double kGradientMaxRatio = 0.9;
// Parse "x0,y0[,m_in0,m_out0]|x1,y1[,m_in1,m_out1]|..." into a GradientCurve.
// (Anchors are pipe-separated; the fields within an anchor are comma-separated.)
// Accepts both the legacy 2-field form (tangents -> NaN) and the new 4-field form
// (empty token or "nan" preserved as NaN). Returns an empty curve when the input is
// empty or unparsable. Points are clamped to [0,1] for (x, y) and re-sorted by x.
GradientCurve parse_gradient_curve(const std::string& s);
// Serialize a GradientCurve back to a string. Emits 4 fields per anchor when any
// tangent override is finite; emits 2 fields when both tangents are NaN so unchanged
// projects stay byte-identical with the legacy format. Returns "" when empty.
std::string serialize_gradient_curve(const GradientCurve& c);
// Sample the curve at t in [0,1] using cubic Hermite with Fritsch-Carlson PCHIP
// default tangents, optionally overridden per anchor via m_in / m_out. Returns the
// clamped end values when t is outside the control point range. Returns 0.5 when the
// curve has fewer than 2 points (a safety fallback; callers should check empty()).
double sample_gradient_curve(const GradientCurve& c, double t);
// Compute Fritsch-Carlson PCHIP default tangents for a sorted-by-x anchor list.
// Result size == pts.size(). Useful for callers that need to know what tangent the
// sampler would synthesize when m_in / m_out are NaN (e.g. the GUI's segment-bend
// interaction that inserts a virtual anchor and reads back the surrounding tangents).
std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts);
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);
// Blend two hex colors ("#RRGGBB") by ratio (0.0 ~ 1.0 for color_b).
// Returns "#RRGGBB" string.
std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b);
// Blend N hex colors by integer weights using polynomial pigment mixing.
// Pairwise accumulation via filament_mixer_lerp. Returns "#RRGGBB".
std::string blend_color_multi(const std::vector<std::string> &hex_colors,
const std::vector<int> &weights);
// Parse comma-separated 1-based component IDs, e.g. "1,3" → {1, 3}.
std::vector<unsigned int> parse_mixed_components(const std::string &str);
// Parse comma-separated ratio values, e.g. "0.7,0.3" → {0.7, 0.3}.
// Returns equal ratios (1/n each) when str is empty or invalid.
// Normalizes so the sum equals 1.0.
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
// Returns true if any element in is_mixed is true.
// ConfigOptionBools stores values as std::vector<unsigned char>.
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
// Check which mixed filament slots have broken component references.
// Returns 0-based indices of mixed slots whose components reference
// filaments beyond num_physical (i.e., deleted filaments).
std::vector<size_t> check_mixed_filament_integrity(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
size_t num_physical);
// Expand mixed filament slots in an extruder list to their physical components.
// Input/output are 0-based indices. Non-mixed slots pass through unchanged.
// Result is sorted and deduplicated.
std::vector<unsigned int> expand_mixed_filaments(
const std::vector<unsigned int> &extruders_0based,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs);
// Remap mixed filament component references after a physical filament is deleted.
// del_1based: the 1-based index of the deleted physical filament.
// For each mixed slot:
// - if component == del_1based -> replace with 0 (sentinel for deleted/unselected)
// - if component > del_1based -> decrement by 1
void remap_mixed_components_on_delete(
const std::vector<unsigned char> &is_mixed,
std::vector<std::string> &comp_strs,
unsigned int del_1based);
// Check which mixed filament slots have type-mismatched components.
// filament_types: type strings for physical filaments (0-based, size == num_physical).
// Component IDs in comp_strs are 1-based; the function converts to 0-based to look up types.
// Returns 0-based config indices of mixed slots with mismatched component types.
std::vector<size_t> check_mixed_filament_type_consistency(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &filament_types);
// Expand mixed-slot IDs in geometric unprintable sets to their physical component IDs.
// Each set entry that corresponds to a mixed slot is replaced by the slot's component
// IDs (0-based). Non-mixed entries pass through unchanged.
void expand_mixed_slots_in_unprintables(
std::vector<std::set<int>> &unprintables,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs);
// Clear any non-empty gradient-curve slot that parses to fewer than 2 control points.
// Heals per-slot arrays corrupted by the legacy "|" separator collision between
// PresetBundle::export_selections / load_selections (which used "|" as the inter-slot
// delimiter) and serialize_gradient_curve / parse_gradient_curve (which use "|" as the
// intra-slot control-point delimiter). Such a round-trip splits a multi-point curve
// across adjacent slots, leaving single-point entries that fail MakerWorld's strict
// "curve needs >= 2 points" check. Clearing them falls back to the linear range.
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals);
// Validate mixed-color (混色) parameters. Returns error messages keyed by option name.
// Slot details are included in the message text (1-based slot index).
std::map<std::string, std::string> validate_mixed_filament_params(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags,
const std::vector<std::string> &gradient_range_strs,
const std::vector<std::string> &gradient_curve_strs);
} // namespace Slic3r
#endif // SLIC3R_FILAMENT_MIXER_HPP
+819
View File
@@ -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 "FilamentMixerModel.hpp"
*
* 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_MODEL_HPP
#define FILAMENT_MIXER_MODEL_HPP
#include <algorithm>
#include <cmath>
#include <cstdint>
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<double>(r1), static_cast<double>(g1), static_cast<double>(b1),
static_cast<double>(r2), static_cast<double>(g2), static_cast<double>(b2),
static_cast<double>(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<int>(sum);
if (val < 0) val = 0;
if (val > 255) val = 255;
if (c == 0) *out_r = static_cast<unsigned char>(val);
else if (c == 1) *out_g = static_cast<unsigned char>(val);
else *out_b = static_cast<unsigned char>(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_MODEL_HPP
@@ -108,6 +108,22 @@ const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
return m_cached_coefficients;
}
bool CornerSmoother::is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next)
{
const Vec2d incoming_leg = vertex - previous;
const Vec2d outgoing_leg = next - vertex;
const double incoming_length = incoming_leg.norm();
const double outgoing_length = outgoing_leg.norm();
// A vertex repeating one of its neighbours carries no direction of its own.
if (incoming_length < EPSILON || outgoing_length < EPSILON)
return true;
const Vec2d incoming = incoming_leg / incoming_length;
const Vec2d outgoing = outgoing_leg / outgoing_length;
return incoming.dot(outgoing) > 0. &&
std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x()) < EPSILON;
}
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
{
m_corner_points.clear();
+41 -18
View File
@@ -1,6 +1,7 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <vector>
@@ -47,36 +48,57 @@ public:
template<typename Emit> void push(const Vec2d &point, Emit &emit)
{
if (m_pending == 0) {
if (m_held == 0) {
// The first point of a path is an end, not a corner, and stays where it is.
emit(point);
m_previous = point;
} else if (m_pending > 1) {
round_corner(m_previous, m_corner, point);
for (const Vec2d &corner_point : m_corner_points)
emit(corner_point);
m_previous = m_corner;
m_window[m_held++] = point;
return;
}
m_corner = point;
m_pending = std::min(m_pending + 1, 2);
if (m_held > 1 && is_on_straight_run(m_window[m_held - 2], m_window[m_held - 1], point)) {
// The newest vertex only splits a straight leg, so the leg runs on to this point instead.
m_window[m_held - 1] = point;
return;
}
if (m_held < 3) {
m_window[m_held++] = point;
return;
}
// Both legs of the middle vertex are complete now, so its curve can no longer grow.
emit_corner(m_window[0], m_window[1], m_window[2], emit);
m_window[0] = m_window[1];
m_window[1] = m_window[2];
m_window[2] = point;
}
// Emits the last point of the path and prepares the smoother for a new one.
template<typename Emit> void flush(Emit &emit)
{
if (m_pending > 1)
emit(m_corner);
m_pending = 0;
if (m_held > 2)
emit_corner(m_window[0], m_window[1], m_window[2], emit);
if (m_held > 1)
emit(m_window[m_held - 1]);
m_held = 0;
}
private:
template<typename Emit> void emit_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next, Emit &emit)
{
round_corner(previous, corner, next);
for (const Vec2d &corner_point : m_corner_points)
emit(corner_point);
}
// Tells a vertex that only continues a straight leg (or repeats its predecessor) from a corner.
// A path doubling back on itself is not one, that vertex is a hairpin and stays where it is.
static bool is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next);
// Fills m_corner_points with the points replacing the corner vertex.
void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next);
// Flattens the canonical corner curve of the given size and turn into coordinates of the
// (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner.
const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing);
// Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment
// is the maximum, otherwise the curves of two adjacent corners would overlap.
// Fraction of the shorter adjoining leg consumed on each side of a corner. Half of a leg is the
// maximum, otherwise the curves of two adjacent corners would overlap.
const double m_corner_distance_ratio;
const double m_tolerance;
const double m_max_corner_distance;
@@ -88,10 +110,11 @@ private:
double m_cached_cosine { 0. };
bool m_has_cached_coefficients { false };
Vec2d m_previous { Vec2d::Zero() };
Vec2d m_corner { Vec2d::Zero() };
// Number of points held back: none, the first point of a path, or a corner candidate.
int m_pending { 0 };
// The corners seen last, kept free of vertices that merely split a straight leg. The middle one
// is rounded once the third arrives, which is what makes its outgoing leg final.
std::array<Vec2d, 3> m_window { Vec2d::Zero(), Vec2d::Zero(), Vec2d::Zero() };
// How many of them are filled in.
int m_held { 0 };
};
// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone.
+327
View File
@@ -0,0 +1,327 @@
#include "AssimpImport.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include <assimp/Importer.hpp>
#include <assimp/config.h>
#include <assimp/material.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <array>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
namespace Slic3r {
namespace {
void clear_textured_mesh(TexturedMesh& out)
{
out.vertices.clear();
out.indices.clear();
out.uvs.clear();
out.uv_coords.clear();
out.uv_indices.clear();
out.textures.clear();
out.material_ids.clear();
out.material_texture_map.clear();
out.material_colors.clear();
}
void set_error_message(std::string* error_message, const std::string& message)
{
if (error_message)
*error_message = message;
}
bool is_fbx_path(const std::string& path)
{
return boost::algorithm::iends_with(path, ".fbx");
}
bool should_flip_uvs(const std::string& path)
{
return boost::algorithm::iends_with(path, ".fbx") ||
boost::algorithm::iends_with(path, ".glb");
}
unsigned int assimp_import_flags(const std::string& path)
{
unsigned int flags = aiProcess_Triangulate
| aiProcess_GenNormals
| aiProcess_PreTransformVertices
| aiProcess_SortByPType;
if (should_flip_uvs(path))
flags |= aiProcess_FlipUVs;
return flags;
}
void configure_importer(Assimp::Importer& importer, const std::string& path, unsigned int flags)
{
importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE,
aiPrimitiveType_POINT | aiPrimitiveType_LINE);
if (flags & aiProcess_PreTransformVertices)
importer.SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true);
if (is_fbx_path(path)) {
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
}
}
bool read_external_texture_file(const boost::filesystem::path& path, TextureImage& out)
{
boost::nowide::ifstream file(path.string(), std::ios::binary | std::ios::ate);
if (!file.is_open())
return false;
const std::streamoff size = file.tellg();
if (size <= 0)
return false;
if (static_cast<uintmax_t>(size) > static_cast<uintmax_t>(std::numeric_limits<size_t>::max()))
return false;
file.seekg(0);
out.width = -1;
out.height = -1;
out.channels = 0;
out.data.resize(static_cast<size_t>(size));
file.read(reinterpret_cast<char*>(out.data.data()), size);
if (!file && !file.eof()) {
out.data.clear();
return false;
}
return true;
}
bool read_embedded_texture(const aiTexture& texture, TextureImage& out)
{
out.data.clear();
if (texture.mHeight == 0) {
if (texture.mWidth == 0)
return false;
out.width = -1;
out.height = -1;
out.channels = 0;
out.data.assign(
reinterpret_cast<const unsigned char*>(texture.pcData),
reinterpret_cast<const unsigned char*>(texture.pcData) + texture.mWidth);
return !out.data.empty();
}
if (texture.mWidth == 0 || texture.mHeight == 0)
return false;
if (texture.mWidth > static_cast<unsigned int>(std::numeric_limits<int>::max()) ||
texture.mHeight > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
return false;
}
const size_t width = static_cast<size_t>(texture.mWidth);
const size_t height = static_cast<size_t>(texture.mHeight);
if (width > std::numeric_limits<size_t>::max() / height ||
width * height > std::numeric_limits<size_t>::max() / 4) {
return false;
}
out.width = static_cast<int>(texture.mWidth);
out.height = static_cast<int>(texture.mHeight);
out.channels = 4;
const size_t pixel_count = width * height;
out.data.resize(pixel_count * 4);
for (size_t i = 0; i < pixel_count; ++i) {
const aiTexel& texel = texture.pcData[i];
out.data[i * 4 + 0] = texel.r;
out.data[i * 4 + 1] = texel.g;
out.data[i * 4 + 2] = texel.b;
out.data[i * 4 + 3] = texel.a;
}
return !out.data.empty();
}
bool get_material_texture(const aiMaterial& material, aiString& texture_path)
{
if (material.GetTextureCount(aiTextureType_DIFFUSE) > 0 &&
material.GetTexture(aiTextureType_DIFFUSE, 0, &texture_path) == AI_SUCCESS) {
return true;
}
if (material.GetTextureCount(aiTextureType_BASE_COLOR) > 0 &&
material.GetTexture(aiTextureType_BASE_COLOR, 0, &texture_path) == AI_SUCCESS) {
return true;
}
return false;
}
std::array<float, 4> get_material_color(const aiMaterial& material)
{
aiColor4D color(1.f, 1.f, 1.f, 1.f);
if (material.Get(AI_MATKEY_BASE_COLOR, color) == AI_SUCCESS)
return {color.r, color.g, color.b, color.a};
if (material.Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS)
return {color.r, color.g, color.b, color.a};
return {1.f, 1.f, 1.f, 1.f};
}
bool collect_mesh(const aiMesh& mesh, size_t& vertex_offset, TexturedMesh& out, std::string& error)
{
if (mesh.mNumVertices > static_cast<size_t>(std::numeric_limits<int>::max()) - vertex_offset) {
error = "Assimp mesh has too many vertices for TexturedMesh indices";
return false;
}
for (unsigned int i = 0; i < mesh.mNumVertices; ++i) {
const aiVector3D& v = mesh.mVertices[i];
out.vertices.push_back({v.x, v.y, v.z});
if (mesh.HasTextureCoords(0)) {
const aiVector3D& uv = mesh.mTextureCoords[0][i];
out.uvs.push_back({uv.x, uv.y});
} else {
out.uvs.push_back({0.f, 0.f});
}
}
const int material_index = static_cast<int>(mesh.mMaterialIndex);
for (unsigned int i = 0; i < mesh.mNumFaces; ++i) {
const aiFace& face = mesh.mFaces[i];
if (face.mNumIndices != 3)
continue;
if (face.mIndices[0] >= mesh.mNumVertices ||
face.mIndices[1] >= mesh.mNumVertices ||
face.mIndices[2] >= mesh.mNumVertices) {
error = "Assimp mesh face index is out of bounds";
return false;
}
out.indices.push_back({
static_cast<int>(static_cast<size_t>(face.mIndices[0]) + vertex_offset),
static_cast<int>(static_cast<size_t>(face.mIndices[1]) + vertex_offset),
static_cast<int>(static_cast<size_t>(face.mIndices[2]) + vertex_offset)});
out.material_ids.push_back(material_index);
}
vertex_offset += mesh.mNumVertices;
return true;
}
void collect_materials(const aiScene& scene, const boost::filesystem::path& base_dir, TexturedMesh& out)
{
out.material_texture_map.assign(scene.mNumMaterials, -1);
out.material_colors.assign(scene.mNumMaterials, {1.f, 1.f, 1.f, 1.f});
for (unsigned int material_index = 0; material_index < scene.mNumMaterials; ++material_index) {
const aiMaterial* material = scene.mMaterials[material_index];
if (!material)
continue;
out.material_colors[material_index] = get_material_color(*material);
aiString texture_path;
if (!get_material_texture(*material, texture_path))
continue;
TextureImage image;
const aiTexture* embedded_texture = scene.GetEmbeddedTexture(texture_path.C_Str());
if (embedded_texture) {
if (!read_embedded_texture(*embedded_texture, image))
continue;
} else {
const boost::filesystem::path resolved = resource_path::resolve_external_resource_path(
base_dir, texture_path.C_Str(), "Assimp texture");
if (resolved.empty()) {
BOOST_LOG_TRIVIAL(warning) << "AssimpImport: texture file not found: "
<< texture_path.C_Str();
continue;
}
if (!read_external_texture_file(resolved, image)) {
BOOST_LOG_TRIVIAL(warning) << "AssimpImport: failed to read texture: "
<< resolved;
continue;
}
}
out.material_texture_map[material_index] = static_cast<int>(out.textures.size());
out.textures.push_back(std::move(image));
}
}
std::string scene_failure_summary(const std::string& path, const char* assimp_error)
{
std::ostringstream ss;
ss << "Assimp failed to import " << path;
if (assimp_error && assimp_error[0] != '\0')
ss << ": " << assimp_error;
return ss.str();
}
} // namespace
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message)
{
clear_textured_mesh(out);
Assimp::Importer importer;
const unsigned int flags = assimp_import_flags(path);
configure_importer(importer, path, flags);
const aiScene* scene = importer.ReadFile(path, flags);
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
const std::string message = scene_failure_summary(path, importer.GetErrorString());
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
return false;
}
if (scene->mNumMeshes == 0) {
const std::string message = "Assimp scene has no meshes: " + path;
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
return false;
}
size_t vertex_offset = 0;
for (unsigned int mesh_index = 0; mesh_index < scene->mNumMeshes; ++mesh_index) {
const aiMesh* mesh = scene->mMeshes[mesh_index];
if (!mesh || !mesh->HasPositions())
continue;
std::string mesh_error;
if (!collect_mesh(*mesh, vertex_offset, out, mesh_error)) {
const std::string message = mesh_error + ": " + path;
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
clear_textured_mesh(out);
return false;
}
}
if (out.vertices.empty() || out.indices.empty()) {
const std::string message = "Assimp extracted no valid triangles: " + path;
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
clear_textured_mesh(out);
return false;
}
collect_materials(*scene, boost::filesystem::path(path).parent_path(), out);
BOOST_LOG_TRIVIAL(info) << "AssimpImport: loaded " << out.vertices.size()
<< " vertices, " << out.indices.size()
<< " triangles, " << out.textures.size()
<< " textures from " << path;
return true;
}
} // namespace Slic3r
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace Slic3r {
struct TexturedMesh;
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr);
} // namespace Slic3r
+147 -3
View File
@@ -1,6 +1,8 @@
#include "../libslic3r.h"
#include "../Model.hpp"
#include "../TriangleMesh.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include "OBJ.hpp"
#include "objparser.hpp"
@@ -21,7 +23,7 @@
namespace Slic3r {
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message)
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl)
{
if (meshptr == nullptr)
return false;
@@ -98,6 +100,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
its.indices.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.usemtls = data.usemtls;
obj_info.face_colors.reserve(num_faces + num_quads);
}
bool has_color = data.has_vertex_color;
@@ -210,14 +213,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
}
if (meshptr->volume() < 0)
meshptr->flip_triangles();
// Hand the parsed material table back so callers can build a TexturedMesh from it.
if (out_mtl)
*out_mtl = mtl_data;
return true;
}
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in)
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl)
{
TriangleMesh mesh;
bool ret = load_obj(path, &mesh, obj_info, message);
bool ret = load_obj(path, &mesh, obj_info, message, out_mtl);
if (ret) {
std::string object_name;
@@ -232,6 +238,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me
return ret;
}
bool obj_to_textured_mesh(
const ObjInfo& obj_info,
const indexed_triangle_set& its,
const ObjParser::MtlData& mtl_data,
const std::string& obj_directory,
TexturedMesh& out)
{
if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png)
return false;
const size_t nv = its.vertices.size();
const size_t nf = its.indices.size();
// 1. Copy vertices
out.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i)
out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
// 2. Copy face indices
out.indices.resize(nf);
for (size_t i = 0; i < nf; ++i)
out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
// 3. Build per-face UV (uv_coords + uv_indices)
// OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV).
// Flip V here so downstream code works uniformly.
if (!obj_info.uvs.empty()) {
const size_t uv_face_count = obj_info.uvs.size();
out.uv_coords.resize(uv_face_count * 3);
out.uv_indices.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
if (fi < uv_face_count) {
int base = static_cast<int>(fi * 3);
out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()};
out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()};
out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()};
out.uv_indices[fi] = {base, base + 1, base + 2};
} else {
out.uv_indices[fi] = {0, 0, 0};
}
}
}
// 4. Build material list and load textures from disk
// Map: material name -> material index
std::map<std::string, int> mtl_name_to_idx;
for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i)
mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i);
const int num_materials = static_cast<int>(mtl_data.mtl_orders.size());
out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f});
out.material_texture_map.resize(num_materials, -1);
// Map: texture filename -> index in out.textures
std::map<std::string, int> png_to_tex_idx;
for (int mi = 0; mi < num_materials; ++mi) {
const std::string& name = mtl_data.mtl_orders[mi];
auto it = mtl_data.new_mtl_unmap.find(name);
if (it == mtl_data.new_mtl_unmap.end())
continue;
const auto& mtl = *(it->second);
// Material color from Kd
out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr};
// Texture from map_Kd
if (mtl.map_Kd.empty())
continue;
auto tex_it = png_to_tex_idx.find(mtl.map_Kd);
if (tex_it != png_to_tex_idx.end()) {
out.material_texture_map[mi] = tex_it->second;
continue;
}
// Resolve texture file path.
const boost::filesystem::path requested_tex_path(mtl.map_Kd);
const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ?
resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") :
resource_path::resolve_existing_relative_path_case_insensitive(
boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd");
if (tex_path.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path;
continue;
}
// Read raw file bytes
boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate);
if (!file.is_open())
continue;
auto file_size = file.tellg();
if (file_size <= 0)
continue;
file.seekg(0, std::ios::beg);
TextureImage ti;
ti.data.resize(static_cast<size_t>(file_size));
file.read(reinterpret_cast<char*>(ti.data.data()), file_size);
ti.width = -1;
ti.height = -1;
ti.channels = 0;
int new_idx = static_cast<int>(out.textures.size());
out.textures.push_back(std::move(ti));
png_to_tex_idx[mtl.map_Kd] = new_idx;
out.material_texture_map[mi] = new_idx;
}
// 5. Build per-face material_ids from usemtls ranges
out.material_ids.resize(nf, -1);
if (!obj_info.usemtls.empty()) {
for (size_t fi = 0; fi < nf; ++fi) {
int face_idx = static_cast<int>(fi);
for (size_t k = 0; k < obj_info.usemtls.size(); ++k) {
const auto& um = obj_info.usemtls[k];
if (face_idx >= um.face_start && face_idx <= um.face_end) {
auto name_it = mtl_name_to_idx.find(um.name);
if (name_it != mtl_name_to_idx.end())
out.material_ids[fi] = name_it->second;
break;
}
}
}
}
if (out.textures.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded";
return false;
}
BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, "
<< out.textures.size() << " textures, "
<< num_materials << " materials";
return true;
}
bool store_obj(const char *path, TriangleMesh *mesh)
{
//FIXME returning false even if write failed.
+14 -2
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_Format_OBJ_hpp_
#define slic3r_Format_OBJ_hpp_
#include "libslic3r/Color.hpp"
#include "objparser.hpp"
#include <unordered_map>
namespace Slic3r {
@@ -18,6 +19,7 @@ struct ObjInfo {
std::map<std::string,bool> pngs;
std::unordered_map<int, std::string> uv_map_pngs;
bool has_uv_png{false};
std::vector<ObjParser::ObjUseMtl> usemtls; // material spans, for texture import
};
struct ObjDialogInOut
@@ -32,8 +34,18 @@ struct ObjDialogInOut
std::string lost_material_name{""};
};
typedef std::function<void(ObjDialogInOut &in_out)> ObjImportColorFn;
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message);
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr);
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message, ObjParser::MtlData *out_mtl = nullptr);
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr, ObjParser::MtlData *out_mtl = nullptr);
struct TexturedMesh;
// Build a TexturedMesh (vertices + per-face UVs + the texture files named by map_Kd) from a
// parsed OBJ plus its material table, so the texture-to-color importer can sample face colours.
extern bool obj_to_textured_mesh(
const ObjInfo& obj_info,
const indexed_triangle_set& its,
const ObjParser::MtlData& mtl_data,
const std::string& obj_directory,
TexturedMesh& out);
extern bool store_obj(const char *path, TriangleMesh *mesh);
extern bool store_obj(const char *path, ModelObject *model);
+240
View File
@@ -0,0 +1,240 @@
#ifndef slic3r_Format_ResourcePathUtils_hpp_
#define slic3r_Format_ResourcePathUtils_hpp_
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace resource_path {
inline std::string ascii_lower_copy(const std::string& value)
{
std::string lowered;
lowered.reserve(value.size());
for (unsigned char ch : value)
lowered.push_back(static_cast<char>(std::tolower(ch)));
return lowered;
}
inline boost::filesystem::path portable_path_copy(const boost::filesystem::path& value)
{
std::string portable = value.string();
std::replace(portable.begin(), portable.end(), '\\', '/');
return boost::filesystem::path(portable);
}
inline int hex_digit_value(char ch)
{
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return -1;
}
// Byte-level percent decoding. Per RFC 3986 the %XX byte stream is expected to be
// UTF-8 when produced from URIs / Assimp aiString; this function performs no
// transcoding, so callers must treat both input and output as raw UTF-8 bytes.
inline std::string percent_decode_copy(const std::string& value)
{
std::string decoded;
decoded.reserve(value.size());
for (std::size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
const int hi = hex_digit_value(value[i + 1]);
const int lo = hex_digit_value(value[i + 2]);
if (hi >= 0 && lo >= 0) {
decoded.push_back(static_cast<char>((hi << 4) | lo));
i += 2;
continue;
}
}
decoded.push_back(value[i]);
}
return decoded;
}
inline std::string strip_file_uri_prefix_copy(const std::string& value)
{
const std::string lower = ascii_lower_copy(value);
if (lower.rfind("file://", 0) != 0)
return value;
std::string path = value.substr(7);
if (ascii_lower_copy(path).rfind("localhost/", 0) == 0)
path.erase(0, std::string("localhost").size());
else if (!path.empty() && path.front() != '/')
path = "//" + path;
// file:///C:/... should become C:/..., while file:///tmp/... keeps /tmp/...
if (path.size() >= 3 && path[0] == '/' && std::isalpha(static_cast<unsigned char>(path[1])) && path[2] == ':')
path.erase(path.begin());
return path;
}
inline bool file_uri_has_remote_authority(const std::string& value)
{
const std::string lower = ascii_lower_copy(value);
if (lower.rfind("file://", 0) != 0)
return false;
const std::string path = value.substr(7);
if (path.empty() || path.front() == '/')
return false;
const std::size_t slash = path.find('/');
const std::string authority = path.substr(0, slash);
return ascii_lower_copy(authority) != "localhost";
}
inline bool looks_like_windows_absolute_path(const boost::filesystem::path& path)
{
const std::string portable = portable_path_copy(path).string();
return portable.size() >= 3
&& std::isalpha(static_cast<unsigned char>(portable[0]))
&& portable[1] == ':'
&& portable[2] == '/';
}
inline boost::filesystem::path filename_from_portable_path(const boost::filesystem::path& value)
{
const boost::filesystem::path portable = portable_path_copy(value);
return portable.filename();
}
inline boost::filesystem::path find_child_case_insensitive(
const boost::filesystem::path& directory,
const boost::filesystem::path& requested_name,
const char* context)
{
if (!boost::filesystem::exists(directory) || !boost::filesystem::is_directory(directory))
return {};
const std::string requested_lower = ascii_lower_copy(requested_name.filename().string());
std::vector<boost::filesystem::path> matches;
boost::system::error_code ec;
for (boost::filesystem::directory_iterator it(directory, ec), end; !ec && it != end; it.increment(ec)) {
if (ascii_lower_copy(it->path().filename().string()) == requested_lower)
matches.push_back(it->path());
}
if (matches.size() == 1)
return matches.front();
if (matches.size() > 1) {
BOOST_LOG_TRIVIAL(warning) << context << ": ambiguous case-insensitive resource match for "
<< requested_name << " in " << directory;
}
return {};
}
inline boost::filesystem::path resolve_existing_path_case_insensitive(
const boost::filesystem::path& requested_path,
const char* context = "resource_path")
{
const boost::filesystem::path normalized_path = portable_path_copy(requested_path);
if (normalized_path.empty())
return {};
if (boost::filesystem::exists(normalized_path))
return normalized_path;
boost::filesystem::path current;
bool initialized = false;
for (const boost::filesystem::path& part : normalized_path) {
if (part == normalized_path.root_name() || part == normalized_path.root_directory()) {
current /= part;
initialized = true;
continue;
}
if (!initialized) {
current = boost::filesystem::current_path();
initialized = true;
}
boost::filesystem::path exact = current / part;
if (boost::filesystem::exists(exact)) {
current = exact;
continue;
}
boost::filesystem::path matched = find_child_case_insensitive(current, part, context);
if (matched.empty())
return {};
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource path case-insensitively from "
<< exact << " to " << matched;
current = matched;
}
return boost::filesystem::exists(current) ? current : boost::filesystem::path();
}
inline boost::filesystem::path resolve_existing_relative_path_case_insensitive(
const boost::filesystem::path& base_dir,
const boost::filesystem::path& resource_path,
const char* context = "resource_path")
{
const boost::filesystem::path requested = resource_path.is_absolute() ? resource_path : base_dir / resource_path;
return resolve_existing_path_case_insensitive(requested, context);
}
// Resolve a resource path that originated outside our own code (e.g. a glTF/FBX
// material texture reference or a file:// URI inside a 3MF descriptor).
//
// `raw_path` is expected to be UTF-8 regardless of host platform: file URIs are
// UTF-8 by spec, and Assimp aiString uses UTF-8 internally. Cross-platform
// correctness on Windows additionally relies on the process having called
// boost::nowide::nowide_filesystem() during startup (see src/BambuStudio.cpp),
// which imbues boost::filesystem::path with a UTF-8 codecvt so that
// `path(std::string)` constructs from UTF-8 byte sequences. Callers that bypass
// the main entry point (standalone CLI tools, unit tests) must reproduce that
// setup themselves before invoking this helper.
inline boost::filesystem::path resolve_external_resource_path(
const boost::filesystem::path& base_dir,
const std::string& raw_path,
const char* context = "resource_path",
bool allow_basename_fallback = true)
{
if (raw_path.empty())
return {};
const bool remote_file_uri = file_uri_has_remote_authority(raw_path);
const std::string decoded_path = percent_decode_copy(strip_file_uri_prefix_copy(raw_path));
const boost::filesystem::path requested = portable_path_copy(boost::filesystem::path(decoded_path));
boost::filesystem::path resolved = (requested.is_absolute() || looks_like_windows_absolute_path(requested)) ?
resolve_existing_path_case_insensitive(requested, context) :
resolve_existing_relative_path_case_insensitive(base_dir, requested, context);
if (!resolved.empty())
return resolved;
if (!allow_basename_fallback || remote_file_uri)
return {};
const boost::filesystem::path basename = filename_from_portable_path(requested);
if (basename.empty())
return {};
resolved = resolve_existing_relative_path_case_insensitive(base_dir, basename, context);
if (!resolved.empty()) {
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource by basename from "
<< requested << " to " << resolved;
}
return resolved;
}
} // namespace resource_path
} // namespace Slic3r
#endif /* slic3r_Format_ResourcePathUtils_hpp_ */
+1 -1
View File
@@ -712,7 +712,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle
return 0;
}
}
} catch(const Exception &e) {
} catch(const Exception &) {
return 0;
}
+42
View File
@@ -5,6 +5,7 @@
#include "../Preset.hpp"
#include "../Utils.hpp"
#include "../LocalesUtils.hpp"
#include "../FilamentMixer.hpp"
#include "../GCode.hpp"
#include "../Geometry.hpp"
#include "../GCode/ThumbnailData.hpp"
@@ -247,6 +248,8 @@ static constexpr const char* BUILD_TAG = "build";
static constexpr const char* ITEM_TAG = "item";
static constexpr const char* METADATA_TAG = "metadata";
static constexpr const char* FILAMENT_TAG = "filament";
static constexpr const char* MIXED_FILAMENT_TAG = "mixed_filament";
static constexpr const char* MIXED_FILAMENT_COMPONENTS_TAG = "components";
static constexpr const char* SLICE_WARNING_TAG = "warning";
static constexpr const char* WARNING_MSG_TAG = "msg";
static constexpr const char *FILAMENT_ID_TAG = "id";
@@ -1318,6 +1321,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_end_config_metadata();
bool _handle_start_config_filament(const char** attributes, unsigned int num_attributes);
bool _handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_filament();
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
@@ -2697,6 +2701,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", load project config file successfully from %1%\n") %dest_file;
// Heal any gradient-curve slots corrupted by the legacy "|" separator collision
// (see FilamentMixer::sanitize_mixed_gradient_curve_array). The 3MF JSON itself
// is safe (";" + C-style escape), but older projects saved through the buggy
// export_selections/load_selections path may already carry single-point entries
// that fail MakerWorld's "curve needs >= 2 points" check.
if (auto* curve_opt = config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
Slic3r::sanitize_mixed_gradient_curve_array(curve_opt->values);
}
}
@@ -3514,6 +3526,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_start_config_plater_instance(attributes, num_attributes);
else if (::strcmp(FILAMENT_TAG, name) == 0)
res = _handle_start_config_filament(attributes, num_attributes);
else if (::strcmp(MIXED_FILAMENT_TAG, name) == 0)
res = _handle_start_config_mixed_filament(attributes, num_attributes);
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
res = _handle_start_config_warning(attributes, num_attributes);
else if (::strcmp(NOZZLE_TAG, name) == 0)
@@ -4693,6 +4707,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes)
{
if (m_curr_plater) {
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_ID_TAG);
std::string type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TYPE_TAG);
std::string color = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_COLOR_TAG);
std::string components = bbs_get_attribute_value_string(attributes, num_attributes, MIXED_FILAMENT_COMPONENTS_TAG);
PlateMixedFilamentInfo mixed_info;
mixed_info.id = atoi(id.c_str());
mixed_info.type = type;
mixed_info.color = color;
mixed_info.components = components;
m_curr_plater->mixed_filaments_info.push_back(mixed_info);
}
return true;
}
bool _BBS_3MF_Importer::_handle_end_config_filament()
{
// do nothing
@@ -8508,6 +8539,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
<< FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n";
}
// Mixed (virtual) filaments used by this plate. These are resolved to physical
// components before g-code statistics, so they are not present in the <filament>
// list above and are recorded separately here.
for (auto it = plate_data->mixed_filaments_info.begin(); it != plate_data->mixed_filaments_info.end(); it++)
{
stream << " <" << MIXED_FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id) << "\" "
<< FILAMENT_TYPE_TAG << "=\"" << it->type << "\" "
<< FILAMENT_COLOR_TAG << "=\"" << it->color << "\" "
<< MIXED_FILAMENT_COMPONENTS_TAG << "=\"" << it->components << "\"/>\n";
}
for (auto it = plate_data->warnings.begin(); it != plate_data->warnings.end(); it++) {
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
}
+14
View File
@@ -48,6 +48,18 @@ public:
};
// Mixed (virtual) filament used by a plate. Mixed filaments are virtual slots that get
// resolved to their physical components before g-code statistics, so they never appear in
// slice_filaments_info. They are recorded here separately so a plate's mixed-color usage
// can be recovered from slice_info.
struct PlateMixedFilamentInfo
{
int id{0}; // 1-based virtual filament slot id
std::string type;
std::string color; // blended display color, "#RRGGBB"
std::string components; // 1-based physical component ids, comma separated, e.g. "1,3"
};
//BBS: define plate data list related structures
struct PlateData
{
@@ -89,6 +101,8 @@ struct PlateData
std::string first_layer_time;
std::string plate_name;
std::vector<FilamentInfo> slice_filaments_info;
// Mixed (virtual) filaments used by this plate; empty when no mixed filament is used.
std::vector<PlateMixedFilamentInfo> mixed_filaments_info;
std::vector<size_t> skipped_objects;
DynamicPrintConfig config;
bool is_support_used {false};
+106 -7
View File
@@ -262,12 +262,9 @@ static bool obj_parseline(const char *line, ObjData &data)
}
face_index_count++;
}
if (face_index_count == 3) {//tri
data.usemtls.back().face_end++;
} else if (face_index_count == 4) {//quad
data.usemtls.back().face_end++;
data.usemtls.back().face_end++;
}
if (face_index_count >= 3) {
data.usemtls.back().face_end += face_index_count - 2;
}
}
vertex.coordIdx = -1;
vertex.normalIdx = -1;
@@ -374,6 +371,107 @@ static bool obj_parseline(const char *line, ObjData &data)
return true;
}
static std::string cur_mtl_name = "";
static bool mtl_is_space(char c)
{
return c == ' ' || c == '\t' || c == '\r';
}
static const char* mtl_skip_ws(const char *line)
{
while (mtl_is_space(*line))
++line;
return line;
}
static const char* mtl_skip_token(const char *line)
{
while (*line != 0 && !mtl_is_space(*line))
++line;
return line;
}
static bool mtl_token_equals(const char *begin, const char *end, const char *token)
{
const size_t len = static_cast<size_t>(end - begin);
return strlen(token) == len && strncmp(begin, token, len) == 0;
}
static std::string mtl_trim_value(const char *line)
{
const char *begin = mtl_skip_ws(line);
const char *end = begin + strlen(begin);
while (end > begin && mtl_is_space(*(end - 1)))
--end;
return std::string(begin, end);
}
static bool mtl_skip_numeric_token(const char *&line)
{
const char *begin = mtl_skip_ws(line);
if (*begin == 0)
return false;
char *endptr = 0;
strtod(begin, &endptr);
if (endptr == begin || (!mtl_is_space(*endptr) && *endptr != 0))
return false;
line = mtl_skip_ws(endptr);
return true;
}
static bool mtl_skip_required_tokens(const char *&line, int count)
{
for (int i = 0; i < count; ++i) {
line = mtl_skip_ws(line);
if (*line == 0)
return false;
line = mtl_skip_token(line);
}
line = mtl_skip_ws(line);
return true;
}
static std::string mtl_parse_texture_name(const char *line)
{
const char *original = mtl_skip_ws(line);
const char *current = original;
while (*current == '-') {
const char *option_begin = current;
const char *option_end = mtl_skip_token(current);
current = option_end;
if (mtl_token_equals(option_begin, option_end, "-o") ||
mtl_token_equals(option_begin, option_end, "-s") ||
mtl_token_equals(option_begin, option_end, "-t")) {
int skipped = 0;
while (skipped < 3 && mtl_skip_numeric_token(current))
++skipped;
if (skipped == 0)
return mtl_trim_value(original);
continue;
}
int option_args = -1;
if (mtl_token_equals(option_begin, option_end, "-mm"))
option_args = 2;
else if (mtl_token_equals(option_begin, option_end, "-bm") ||
mtl_token_equals(option_begin, option_end, "-boost") ||
mtl_token_equals(option_begin, option_end, "-texres") ||
mtl_token_equals(option_begin, option_end, "-clamp") ||
mtl_token_equals(option_begin, option_end, "-blendu") ||
mtl_token_equals(option_begin, option_end, "-blendv") ||
mtl_token_equals(option_begin, option_end, "-cc") ||
mtl_token_equals(option_begin, option_end, "-imfchan") ||
mtl_token_equals(option_begin, option_end, "-type"))
option_args = 1;
if (option_args < 0 || !mtl_skip_required_tokens(current, option_args))
return mtl_trim_value(original);
}
return mtl_trim_value(current);
}
static bool mtl_parseline(const char *line, MtlData &data)
{
if (*line == 0) return true;
@@ -394,13 +492,14 @@ static bool mtl_parseline(const char *line, MtlData &data)
ObjNewMtl new_mtl;
cur_mtl_name = line;
data.new_mtl_unmap[cur_mtl_name] = std::make_shared<ObjNewMtl>();
data.mtl_orders.emplace_back(cur_mtl_name);
break;
}
case 'm': {
if (*(line++) != 'a' || *(line++) != 'p' || *(line++) != '_' || *(line++) != 'K' || *(line++) != 'd') return false;
EATWS();
if (data.new_mtl_unmap.find(cur_mtl_name) != data.new_mtl_unmap.end()) {
data.new_mtl_unmap[cur_mtl_name]->map_Kd = line;
data.new_mtl_unmap[cur_mtl_name]->map_Kd = mtl_parse_texture_name(line);
}
break;
}
+3
View File
@@ -122,6 +122,9 @@ struct MtlData
// Version of the data structure for load / store in the private binary format.
int version;
std::unordered_map<std::string, std::shared_ptr<ObjNewMtl>> new_mtl_unmap;
// Material names in declaration order. new_mtl_unmap is unordered, but OBJ material
// indices are positional, so texture import needs the original order.
std::vector<std::string> mtl_orders;
};
extern bool objparse(const char *path, ObjData &data);
extern bool mtlparse(const char *path, MtlData &data);
+361 -8
View File
@@ -4340,6 +4340,8 @@ void GCode::export_layer_filaments(GCodeProcessorResult* result)
}
}
result->used_mixed_filaments = m_print->get_slice_used_mixed_filaments();
result->optimal_assignment.clear();
result->optimal_assignment.reserve(filament_map.size());
for (int nozzle_id : filament_map)
@@ -6181,9 +6183,16 @@ LayerResult GCode::process_layer(
const WipingExtrusions::ExtruderPerCopy *entity_overrides = nullptr;
if (! layer_tools.has_extruder(correct_extruder_id)) {
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
correct_extruder_id = layer_tools.extruders.back();
// A mixed-color slot is absent from layer_tools.extruders by design:
// resolve_mixed_filaments() replaced it with its physical components,
// and the sublayer block emits its geometry separately. Reassigning it
// to the last extruder here would print it in the wrong colour, so only
// fall back for genuinely stale (dontcare) extruders.
if (!layer_tools.is_mixed_slot(correct_extruder_id)) {
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
correct_extruder_id = layer_tools.extruders.back();
}
}
printing_extruders.clear();
if (is_anything_overridden && use_overrides) {
@@ -6271,7 +6280,16 @@ LayerResult GCode::process_layer(
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
single_object_instance_idx == size_t(-1) &&
print.config().print_order != PrintOrder::AsObjectList;
for (unsigned int filament_id : layer_tools.extruders) {
// A mixed-color slot is absent from layer_tools.extruders by design: resolve_mixed_filaments()
// replaced it with its physical components. Its geometry is still keyed under the slot in
// by_extruder though, and the sublayer emitter looks the plan up by slot id, so append the
// slots here. Appending rather than merging leaves the flush-optimized order untouched.
std::vector<unsigned int> plan_filaments = layer_tools.extruders;
for (const auto &grp : layer_tools.mixed_sub_layer_groups)
if (std::find(plan_filaments.begin(), plan_filaments.end(), grp.mixed_slot_0based) == plan_filaments.end())
plan_filaments.push_back(grp.mixed_slot_0based);
for (unsigned int filament_id : plan_filaments) {
auto objects_by_extruder_it = by_extruder.find(filament_id);
if (objects_by_extruder_it == by_extruder.end()) continue;
@@ -6452,8 +6470,22 @@ LayerResult GCode::process_layer(
}
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
std::vector<size_t> filament_instances_id;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
std::set<size_t> all_label_ids;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first)
all_label_ids.insert(instance.label_object_id);
// This extruder may also be printing sub-layers on behalf of a mixed slot, whose
// instances live under the slot id. Their labels belong in the same skip set, or
// exclude-object would not skip that geometry.
for (const auto &grp : layer_tools.mixed_sub_layer_groups)
for (unsigned int comp : grp.components_0based)
if (comp == extruder_id) {
auto mit = filament_to_print_instances.find(grp.mixed_slot_0based);
if (mit != filament_to_print_instances.end())
for (const InstanceToPrint &inst : mit->second.first)
all_label_ids.insert(inst.label_object_id);
break;
}
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
}
@@ -6734,6 +6766,318 @@ LayerResult GCode::process_layer(
}
}
}
// Mixed-color sublayer extrusion: if this extruder is a component of a mixed sublayer
// group, extrude the mixed slot's geometry at the appropriate sub-Z with scaled flow.
// Ported from BambuStudio and adapted to Orca's instance loop and its finer-grained
// per-role region filament options.
for (const auto &grp : layer_tools.mixed_sub_layer_groups) {
int sub_idx = -1;
for (size_t k = 0; k < grp.components_0based.size(); ++k) {
if (grp.components_0based[k] == extruder_id) {
sub_idx = static_cast<int>(k);
break;
}
}
if (sub_idx < 0)
continue;
auto mixed_instances_it = filament_to_print_instances.find(grp.mixed_slot_0based);
if (mixed_instances_it == filament_to_print_instances.end() || mixed_instances_it->second.first.empty())
continue;
double lh = grp.layer_height > 0. ? grp.layer_height : static_cast<double>(height);
double cumulative_h = 0.0;
for (int i = 0; i < sub_idx; ++i)
cumulative_h += grp.sub_heights[i];
double default_sub_h = grp.sub_heights[sub_idx];
double default_sub_z = print_z - lh + cumulative_h + default_sub_h;
m_sub_layer_flow_ratio = default_sub_h / lh;
m_sub_layer_height = default_sub_h;
m_nominal_z = default_sub_z;
gcode += this->set_extruder(extruder_id, default_sub_z);
for (InstanceToPrint &instance_to_print : mixed_instances_it->second.first) {
const bool use_per_volume = grp.is_gradient
&& !grp.per_volume_gradient.empty()
&& std::any_of(grp.per_volume_gradient.begin(), grp.per_volume_gradient.end(),
[&](const auto &kv) { return kv.first.obj == &instance_to_print.print_object; });
// --- Shared instance preamble (mirrors Orca's main instance loop) ---
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
const auto &inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
bool object_layer_over_raft = layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 &&
instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id();
m_config.apply(print.default_region_config());
m_config.apply(instance_to_print.print_object.config(), true);
m_layer = layer_to_print.layer();
m_object_layer_over_raft = object_layer_over_raft;
if (m_config.reduce_crossing_wall)
m_avoid_crossing_perimeters.init_layer(*m_layer);
if (this->config().gcode_label_objects) {
gcode += std::string("; printing object ") + instance_to_print.print_object.model_object()->name +
" id:" + std::to_string(instance_to_print.print_object.get_id()) + " copy " +
std::to_string(inst.id) + "\n";
}
if (m_enable_exclude_object) {
if (is_BBL_Printer()) {
m_writer.set_object_start_str(
std::string("; start printing object, unique label id: ") +
std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " +
_encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n");
} else {
const auto gflavor = print.config().gcode_flavor.value;
if (gflavor == gcfKlipper) {
m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") +
get_instance_name(&instance_to_print.print_object, inst.id) + "\n");
} else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) {
m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n");
}
}
}
m_extrusion_quality_estimator.set_current_object(&instance_to_print.print_object);
const Point &offset = inst.shift;
std::pair<const PrintObject*, Point> this_object_copy(&instance_to_print.print_object, offset);
if (m_last_obj_copy != this_object_copy)
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
// --- Build emission plan ---
// Each entry represents one travel_to_z + extrude pass. Per-object mode produces
// exactly 1 entry (all regions, single sub_z); per-volume mode produces N entries
// for tagged volumes plus an optional entry for untagged residue.
struct SubLayerEmitEntry {
double sub_h;
double sub_z;
std::function<bool(size_t region_idx)> region_filter;
bool skip = false;
};
std::vector<SubLayerEmitEntry> emit_plan;
auto compute_sub_zh = [&](double r1, double r2, double &out_sub_h, double &out_sub_z) {
std::vector<double> sub_heights_local(grp.components_0based.size());
for (size_t ci = 0; ci < grp.components_0based.size(); ++ci)
sub_heights_local[ci] = (static_cast<int>(ci) == grp.gradient_first_sorted_idx) ? r1 * lh : r2 * lh;
double cum = 0.0;
for (int ci = 0; ci < sub_idx; ++ci)
cum += sub_heights_local[ci];
out_sub_h = sub_heights_local[sub_idx];
out_sub_z = print_z - lh + cum + out_sub_h;
};
auto gradient_ratios = [](const auto &g) -> std::pair<double, double> {
double t = (g.total_layers > 0) ? (2.0 * g.current_idx + 1.0) / (2.0 * g.total_layers) : 0.5;
// Custom curve wins over linear range when present; OFF path stays bit-identical.
double r1 = g.curve.empty()
? (g.gradient_start + (g.gradient_end - g.gradient_start) * t)
: sample_gradient_curve(g.curve, t);
return {r1, 1.0 - r1};
};
// Orca splits BBS's three role filaments into five; a region belongs to the slot
// when any of its roles is assigned to it.
auto region_uses_slot = [](const PrintRegionConfig &rcfg, unsigned int slot_1b) {
return (unsigned int)rcfg.outer_wall_filament_id.value == slot_1b
|| (unsigned int)rcfg.inner_wall_filament_id.value == slot_1b
|| (unsigned int)rcfg.sparse_infill_filament_id.value == slot_1b
|| (unsigned int)rcfg.internal_solid_filament_id.value == slot_1b
|| (unsigned int)rcfg.top_surface_filament_id.value == slot_1b
|| (unsigned int)rcfg.bottom_surface_filament_id.value == slot_1b;
};
double obj_sub_z = default_sub_z;
if (use_per_volume) {
const PrintObject *po = &instance_to_print.print_object;
const unsigned int slot_1b = grp.mixed_slot_0based + 1;
// Discover tagged volumes and untagged presence for this instance.
std::set<ObjectID> tagged_volumes_present;
bool has_untagged_for_slot = false;
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
for (size_t r = 0; r < island.by_region.size(); ++r) {
const auto &region = island.by_region[r];
if (region.perimeters.empty() && region.infills.empty())
continue;
const PrintRegion &pr = print.get_print_region(r);
if (!region_uses_slot(pr.config(), slot_1b))
continue;
ObjectID vid = pr.gradient_volume_id();
if (vid.valid())
tagged_volumes_present.insert(vid);
else
has_untagged_for_slot = true;
}
}
// One entry per tagged volume.
for (const ObjectID &target_vid : tagged_volumes_present) {
auto vg_it = grp.per_volume_gradient.find({po, target_vid});
if (vg_it == grp.per_volume_gradient.end())
continue;
const auto &vg = vg_it->second;
auto [r1, r2] = gradient_ratios(vg);
bool vol_no_split = false;
bool skip_entry = false;
const size_t n = grp.components_0based.size();
if (n == 2 && vg.current_idx + 1 == vg.total_layers) {
const size_t dom_idx = (r1 >= r2) ? 0 : 1;
const unsigned int first_sorted_comp = grp.components_0based[grp.gradient_first_sorted_idx];
const unsigned int other_comp = grp.components_0based[1 - grp.gradient_first_sorted_idx];
const unsigned int dom_0b = (dom_idx == 0) ? first_sorted_comp : other_comp;
const unsigned int oth_0b = (dom_idx == 0) ? other_comp : first_sorted_comp;
if (dom_0b < oth_0b) {
vol_no_split = true;
if (extruder_id != dom_0b)
skip_entry = true;
}
}
double vol_sub_h = default_sub_h;
double vol_sub_z = default_sub_z;
if (vol_no_split) {
vol_sub_h = lh;
vol_sub_z = print_z;
} else {
compute_sub_zh(r1, r2, vol_sub_h, vol_sub_z);
}
emit_plan.push_back({vol_sub_h, vol_sub_z,
[target_vid, &print](size_t r) {
return print.get_print_region(r).gradient_volume_id() == target_vid;
},
skip_entry});
}
// Optional entry for untagged regions (modifier / painted / fuzzy_skin).
if (has_untagged_for_slot) {
double obj_sub_h = default_sub_h;
auto og_it = grp.per_object_gradient.find(po);
if (og_it != grp.per_object_gradient.end()) {
auto [r1, r2] = gradient_ratios(og_it->second);
compute_sub_zh(r1, r2, obj_sub_h, obj_sub_z);
}
emit_plan.push_back({obj_sub_h, obj_sub_z,
[&print](size_t r) {
return !print.get_print_region(r).gradient_volume_id().valid();
},
false});
}
} else {
// Legacy per-object path: single entry, no region filter.
double legacy_sub_h = default_sub_h;
obj_sub_z = default_sub_z;
if (grp.is_gradient) {
auto og_it = grp.per_object_gradient.find(&instance_to_print.print_object);
if (og_it != grp.per_object_gradient.end()) {
auto [r1, r2] = gradient_ratios(og_it->second);
compute_sub_zh(r1, r2, legacy_sub_h, obj_sub_z);
}
}
emit_plan.push_back({legacy_sub_h, obj_sub_z, nullptr, false});
}
// --- Unified emission loop ---
auto plan_has_infill = [](const std::vector<ObjectByExtruder::Island::Region> &by_region) {
for (const auto &r : by_region)
if (!r.infills.empty())
return true;
return false;
};
for (auto &entry : emit_plan) {
if (entry.skip)
continue;
m_sub_layer_flow_ratio = entry.sub_h / lh;
m_sub_layer_height = entry.sub_h;
m_nominal_z = entry.sub_z;
// Use the same lazy-Z mechanism as change_layer(): set the flag so travel_to
// fires even when m_last_pos coincides with the first extrusion point,
// ensuring Z reaches sub_z via the combined XY+Z move.
m_need_change_layer_lift_z = true;
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
const auto &src = island.by_region;
std::vector<ObjectByExtruder::Island::Region> subset_storage;
if (entry.region_filter) {
subset_storage.resize(src.size());
for (size_t r = 0; r < src.size(); ++r)
if (entry.region_filter(r))
subset_storage[r] = src[r];
}
const auto &by_region_specific = entry.region_filter ? subset_storage : src;
// Orca resolves infill-first per region inside extrude_perimeters()
// (unlike BBS, which branches on a single global flag), so mirror the
// main instance loop's ordering exactly.
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false);
if (!has_wipe_tower && need_insert_timelapse_gcode_for_traditional
&& printer_structure == PrinterStructure::psI3
&& !has_insert_timelapse_gcode && plan_has_infill(by_region_specific)) {
gcode += this->retract(false, false, auto_lift_type, true);
gcode += insert_timelapse_gcode();
has_insert_timelapse_gcode = true;
}
gcode += this->extrude_infill(print, by_region_specific, false);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
// ironing
gcode += this->extrude_infill(print, by_region_specific, true);
}
}
// --- Shared support ---
if (instance_to_print.object_by_extruder.support && !instance_to_print.object_by_extruder.support->empty()) {
if (use_per_volume) {
m_nominal_z = obj_sub_z;
m_need_change_layer_lift_z = true;
}
ExtrusionRole support_role = instance_to_print.object_by_extruder.support_extrusion_role;
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, support_role);
// Make sure ironing is the last (Orca names this role erIroning, not erSupportIroning).
if (support_role == erMixed || support_role == erSupportMaterialInterface)
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, erIroning);
}
// --- Shared instance footer (mirrors Orca's main instance loop) ---
if (!m_writer.is_object_start_str_empty()) {
m_writer.set_object_start_str("");
} else if (m_enable_exclude_object) {
if (is_BBL_Printer()) {
m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") +
std::to_string(instance_to_print.label_object_id) + "\n" +
"M625\n");
} else {
const auto gflavor = print.config().gcode_flavor.value;
if (gflavor == gcfKlipper) {
m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") +
get_instance_name(&instance_to_print.print_object, inst.id) + "\n");
} else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) {
m_writer.set_object_end_str(std::string("M486 S-1\n"));
}
}
}
}
m_sub_layer_flow_ratio = 0.0;
m_sub_layer_height = 0.0;
}
// Flush any pending object end label before leaving the sublayer block, otherwise the
// wipe tower's add_object_end_labels may consume it into a local temp string and the
// M625 would be lost for BBL printers.
if (!layer_tools.mixed_sub_layer_groups.empty()) {
m_writer.add_object_end_labels(gcode);
m_nominal_z = print_z;
m_need_change_layer_lift_z = true;
}
}
if (first_layer) {
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
@@ -7811,6 +8155,15 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
}
}
// Mixed-color sublayer: this path belongs to one sub-layer of a split layer, so scale the
// flow down to that sub-layer's share of the nominal layer height and report the sub-height
// as the effective extrusion height. Inert (ratio == 0) outside the sublayer emission block.
float effective_height = path.height;
if (m_sub_layer_flow_ratio > 0.0) {
_mm3_per_mm *= m_sub_layer_flow_ratio;
effective_height = static_cast<float>(m_sub_layer_height);
}
// Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio
// m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area)
double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm;
@@ -8110,8 +8463,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
gcode += buf;
}
if (last_was_wipe_tower || std::abs(m_last_height - path.height) > EPSILON) {
m_last_height = path.height;
if (last_was_wipe_tower || std::abs(m_last_height - effective_height) > EPSILON) {
m_last_height = effective_height;
sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), m_last_height);
gcode += buf;
}
+5
View File
@@ -764,6 +764,11 @@ private:
Print* m_curr_print = nullptr;
unsigned int m_toolchange_count;
coordf_t m_nominal_z;
// Mixed-color sublayer state. Non-zero only while emitting a mixed slot's sub-layer:
// scales extrusion flow to the sub-layer's share of the nominal layer height, and
// reports that sub-height as the effective extrusion height. Reset to 0 afterwards.
double m_sub_layer_flow_ratio = 0.0;
double m_sub_layer_height = 0.0;
bool m_need_change_layer_lift_z = false;
int m_start_gcode_filament = -1;
std::string m_filament_instances_code;
+1
View File
@@ -2543,6 +2543,7 @@ void GCodeProcessorResult::reset() {
spiral_vase_mode = false;
layer_filaments.clear();
filament_change_sequence.clear();
used_mixed_filaments.clear();
nozzle_change_sequence.clear();
optimal_assignment.clear();
filament_change_count_map.clear();
+4
View File
@@ -306,6 +306,9 @@ class Print;
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
std::vector<unsigned int> nozzle_change_sequence;
std::vector<unsigned int> filament_change_sequence;
// 0-based mixed (virtual) filament slots actually used on this plate.
// Recorded before resolve_mixed_filaments expands them to physical components.
std::vector<unsigned int> used_mixed_filaments;
std::vector<int> optimal_assignment;
// first key stores `from` filament, second keys stores the `to` filament
std::map<std::pair<int,int>, int > filament_change_count_map;
@@ -357,6 +360,7 @@ class Print;
printer_extruder_id = other.printer_extruder_id;
layer_filaments = other.layer_filaments;
filament_change_sequence = other.filament_change_sequence;
used_mixed_filaments = other.used_mixed_filaments;
nozzle_change_sequence = other.nozzle_change_sequence;
optimal_assignment = other.optimal_assignment;
filament_change_count_map = other.filament_change_count_map;
+1 -1
View File
@@ -32,7 +32,7 @@ using ThumbnailsList = std::vector<ThumbnailData>;
struct ThumbnailsParams
{
const Vec2ds sizes;
const Vec2ds sizes{};
bool printable_only;
bool parts_only;
bool show_bed;
+803 -5
View File
@@ -7,6 +7,8 @@
#include "GCode/ToolOrderUtils.hpp"
#include "FilamentGroupUtils.hpp"
#include "MultiNozzleUtils.hpp"
#include "FilamentMixer.hpp"
#include "LocalesUtils.hpp"
#include "Utils.hpp"
#include "I18N.hpp"
@@ -22,8 +24,13 @@
#endif
#include <cassert>
#include <cstdio>
#include <limits>
#include <algorithm>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <unordered_map>
#include <libslic3r.h>
@@ -84,22 +91,28 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
}
// Return a zero based extruder from the region, or extruder_override if overriden.
// The region accessors below resolve mixed-color slots to the physical filament chosen for this
// layer by resolve_mixed_filaments(), because a virtual slot id is never a real tool. resolve_mixed()
// returns its argument unchanged for every filament that is not a mixed slot.
unsigned int LayerTools::wall_extruder_id(const PrintRegion &region) const
{
assert(region.config().outer_wall_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1;
unsigned int result = ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1;
return resolve_mixed(result);
}
unsigned int LayerTools::sparse_infill_filament_id(const PrintRegion &region) const
{
assert(region.config().sparse_infill_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1;
unsigned int result = ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1;
return resolve_mixed(result);
}
unsigned int LayerTools::internal_solid_filament_id(const PrintRegion &region) const
{
assert(region.config().internal_solid_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
unsigned int result = ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
return resolve_mixed(result);
}
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
@@ -135,7 +148,8 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c
} else
extruder = this->extruder_override;
return (extruder == 0) ? 0 : extruder - 1;
unsigned int result = (extruder == 0) ? 0 : extruder - 1;
return resolve_mixed(result);
}
static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height)
@@ -402,7 +416,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
// so we shouldn't reorder first layer in reorder function
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
this->resolve_mixed_filaments(print.config());
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
this->enforce_mixed_component_order();
m_sorted = true;
double max_layer_height = 0.;
@@ -422,6 +438,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
if (this->insert_wipe_tower_extruder()) {
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
// Orca reorders a second time here (BBS has no such path); re-enforce so the
// mixed sub-layer component order survives the extra pass.
this->enforce_mixed_component_order();
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
}
@@ -433,7 +452,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
// so we shouldn't reorder first layer in reorder function
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
this->resolve_mixed_filaments(object.print()->config());
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
this->enforce_mixed_component_order();
m_sorted = true;
double max_layer_height = calc_max_layer_height(object.print()->config(), object.config().layer_height);
@@ -441,6 +462,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
if (this->insert_wipe_tower_extruder()) {
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
// Orca reorders a second time here (BBS has no such path); re-enforce so the
// mixed sub-layer component order survives the extra pass.
this->enforce_mixed_component_order();
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
}
@@ -723,6 +747,38 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
it_per_layer_extruder_override = per_layer_extruder_switches.begin();
unsigned int extruder_override = 0;
// Pre-compute 1-based IDs of mixed filament slots for per-object tracking.
// mixed_slots_1based covers ALL mixed slots (needed by calc_slot_lh for
// accurate layer height when a slot skips layers). gradient_slots_1based
// and per_part_slots_1based are subsets for gradient-specific logic.
std::set<unsigned int> mixed_slots_1based;
std::set<unsigned int> gradient_slots_1based;
std::set<unsigned int> per_part_slots_1based;
{
const PrintConfig &cfg = object.print()->config();
const auto &is_mixed = cfg.filament_is_mixed.values;
const auto &grad_flags = cfg.filament_mixed_gradient.values;
const auto &per_part_flags = cfg.filament_mixed_gradient_per_part.values;
const auto &comp_strs = cfg.filament_mixed_components.values;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
auto comps = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
if (comps.size() < 2)
continue;
mixed_slots_1based.insert(static_cast<unsigned int>(i + 1));
// Gradient/per-part are only defined for 2-component slots; keep their
// tracking limited to them (mirrors the is_gradient guard at resolve time).
if (comps.size() != 2)
continue;
if (i >= grad_flags.size() || !grad_flags[i])
continue;
gradient_slots_1based.insert(static_cast<unsigned int>(i + 1));
if (i < per_part_flags.size() && per_part_flags[i])
per_part_slots_1based.insert(static_cast<unsigned int>(i + 1));
}
}
// BBS: collect first layer extruders of an object's wall, which will be used by brim generator
int layerCount = 0;
std::vector<int> firstLayerExtruders;
@@ -732,6 +788,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
for (auto layer : object.layers()) {
LayerTools &layer_tools = this->tools_for_layer(layer->print_z);
m_object_all_layer_indices[&object].push_back(
static_cast<size_t>(&layer_tools - m_layer_tools.data()));
// Override extruder with the next
for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override)
extruder_override = (int)it_per_layer_extruder_override->second;
@@ -739,6 +798,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
// Store the current extruder override (set to zero if no overriden), so that layer_tools.wiping_extrusions().is_overridable_and_mark() will use it.
layer_tools.extruder_override = extruder_override;
// Snapshot extruders before this object's regions to track new additions.
const size_t ext_snapshot = layer_tools.extruders.size();
// What extruders are required to print this object layer?
for (const LayerRegion *layerm : layer->regions()) {
const PrintRegion &region = layerm->region();
@@ -805,6 +867,54 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill)
layer_tools.has_object = true;
}
// Record mixed slot usage for this object at this layer.
// All mixed slots are tracked (not just gradient) so that calc_slot_lh
// can compute accurate layer heights even when a slot skips layers.
if (!mixed_slots_1based.empty()) {
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
std::set<unsigned int> seen;
for (size_t ei = ext_snapshot; ei < layer_tools.extruders.size(); ++ei) {
unsigned int ext_1based = layer_tools.extruders[ei];
if (mixed_slots_1based.count(ext_1based) && seen.insert(ext_1based).second)
m_mixed_object_layers[ext_1based - 1][&object].push_back(layer_idx);
}
}
// Per-part gradient: walk LayerRegions and record which (slot, ModelVolume) pairs
// contributed to this layer. Only regions tagged by PrintApply.cpp's get_create_region
// (i.e. gradient_volume_id().valid()) are considered, so this loop is a strict no-op
// unless per_part_gradient is enabled for at least one slot AND the corresponding
// ModelObject has >=2 model-part volumes using that slot. The per-object pass above is
// unaffected — both run the same layer's data through orthogonal containers.
if (!per_part_slots_1based.empty()) {
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
std::set<std::pair<unsigned int, ObjectID>> vol_seen;
for (const LayerRegion *layerm : layer->regions()) {
if (layerm->slices.empty())
continue;
const PrintRegion &region = layerm->region();
ObjectID vol_id = region.gradient_volume_id();
if (! vol_id.valid())
continue;
const PrintRegionConfig &rcfg = region.config();
// Orca splits BBS's three role slots into five; cover them all so a mixed
// slot used by any role is tracked.
const unsigned int role_slots[5] = {
static_cast<unsigned int>(rcfg.outer_wall_filament_id.value),
static_cast<unsigned int>(rcfg.inner_wall_filament_id.value),
static_cast<unsigned int>(rcfg.sparse_infill_filament_id.value),
static_cast<unsigned int>(rcfg.top_surface_filament_id.value),
static_cast<unsigned int>(rcfg.bottom_surface_filament_id.value),
};
for (unsigned int ext_1based : role_slots) {
if (ext_1based >= 1
&& per_part_slots_1based.count(ext_1based)
&& vol_seen.insert({ext_1based, vol_id}).second)
m_gradient_volume_layers[ext_1based - 1][{&object, vol_id}].push_back(layer_idx);
}
}
}
layerCount++;
}
@@ -903,7 +1013,7 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
//FIXME this is a hack to get the ball rolling.
for (LayerTools &lt : m_layer_tools)
lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|| lt.print_z < object_bottom_z + EPSILON;
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
@@ -944,6 +1054,84 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
}
}
// Ensure wipe tower vertical continuity:
//
// (1) Any existing LayerTools sandwiched between two has_wipe_tower layers must itself be a
// wipe-tower layer. The LayerTools entry already exists, but it has neither object nor
// support geometry (has_object == false && has_support == false), so the marking pass
// above leaves has_wipe_tower == false. Happens e.g. when one object is fully floating
// above another and the support_top_z_distance / support_bottom_z_distance gap leaves an
// interior layer with no object and no support (e.g. B top z=20.4, A first layer z=20.8,
// the z=20.6 LayerTools entry exists but stays unmarked).
//
// (2) When two adjacent has_wipe_tower layers are farther apart than max_layer_height and no
// LayerTools entry exists between them, insert virtual wipe-tower-only layers to bridge
// the gap. Happens with raft: BambuStudio's raft contact layer can be thicker than
// max_layer_height (e.g. raft base top z=0.2, raft contact top z=0.5 — gap 0.3 > 0.28),
// and there is no LayerTools entry between those two z values.
//
// wipe_tower_partitions has already been max-propagated downward above, so partition counts
// on the filled-in / inserted layers stay consistent.
{
int first_wt_idx = -1;
int last_wt_idx = -1;
for (int i = 0; i < (int)m_layer_tools.size(); ++i)
if (m_layer_tools[i].has_wipe_tower) {
if (first_wt_idx < 0) first_wt_idx = i;
last_wt_idx = i;
}
for (int i = first_wt_idx + 1; i < last_wt_idx; ++i) {
LayerTools &lt = m_layer_tools[i];
lt.has_wipe_tower = true;
// GCode::process_layer emits wipe-tower G-code inside `for (extruder_id : layer_tools.extruders)`.
// An empty extruders vector here would silently skip wipe tower output, leaving the tower
// physically floating. Seed from the nearest non-empty neighbor so the loop actually runs.
if (lt.extruders.empty()) {
unsigned int seed_extruder = 0;
bool found_seed = false;
for (int j = i - 1; j >= 0; --j)
if (!m_layer_tools[j].extruders.empty()) {
seed_extruder = m_layer_tools[j].extruders.back();
found_seed = true;
break;
}
if (!found_seed)
for (int j = i + 1; j < (int)m_layer_tools.size(); ++j)
if (!m_layer_tools[j].extruders.empty()) {
seed_extruder = m_layer_tools[j].extruders.front();
found_seed = true;
break;
}
if (found_seed)
lt.extruders.push_back(seed_extruder);
}
}
// Walk adjacent has_wipe_tower pairs and split oversized gaps. Re-evaluate the same i
// after each insertion so very large gaps get split into multiple layers.
for (int i = 0; i + 1 < (int)m_layer_tools.size(); ) {
LayerTools &lt = m_layer_tools[i];
LayerTools &lt_next = m_layer_tools[i + 1];
if (!lt.has_wipe_tower || !lt_next.has_wipe_tower) {
++i;
continue;
}
coordf_t gap = lt_next.print_z - lt.print_z;
if (gap <= max_layer_height + EPSILON) {
++i;
continue;
}
LayerTools lt_new(0.5 * (lt.print_z + lt_next.print_z));
lt_new.has_wipe_tower = true;
if (!lt_next.extruders.empty())
lt_new.extruders.push_back(lt_next.extruders.front());
else if (!lt.extruders.empty())
lt_new.extruders.push_back(lt.extruders.back());
lt_new.wipe_tower_partitions = lt_next.wipe_tower_partitions;
m_layer_tools.insert(m_layer_tools.begin() + i + 1, lt_new);
}
}
// If the model contains empty layers (such as https://github.com/prusa3d/Slic3r/issues/1266), there might be layers
// that were not marked as has_wipe_tower, even when they should have been. This produces a crash with soluble supports
// and maybe other problems. We will therefore go through layer_tools and detect and fix this.
@@ -1945,6 +2133,605 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_
return result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult();
}
static double snap_to_simple_fraction(double r, int max_denom = 10)
{
double best_r = r;
double best_err = 1.0;
for (int q = 1; q <= max_denom; ++q) {
int p = (int)std::round(r * q);
if (p < 0) p = 0;
if (p > q) p = q;
double candidate = (double)p / q;
double err = std::abs(candidate - r);
if (err < best_err) {
best_err = err;
best_r = candidate;
}
}
return best_r;
}
void ToolOrdering::resolve_mixed_filaments(const PrintConfig &config)
{
const auto &is_mixed = config.filament_is_mixed.values;
const auto &comp_strs = config.filament_mixed_components.values;
const auto &ratio_strs = config.filament_mixed_sublayer_ratios.values;
// Capture mixed slots that actually appear on layers before they are expanded to
// physical components. Assigned-but-unused mixed slots never enter layer_tools.
m_used_mixed_filaments.clear();
if (has_any_mixed_filament(is_mixed)) {
std::set<unsigned int> used;
for (const LayerTools &lt : m_layer_tools)
for (unsigned int ext : lt.extruders)
if (ext < is_mixed.size() && is_mixed[ext])
used.insert(ext);
m_used_mixed_filaments.assign(used.begin(), used.end());
}
if (!has_any_mixed_filament(is_mixed))
return;
const bool sublayer_enabled = config.enable_mixed_color_sublayer.value;
struct SlotInfo {
std::vector<unsigned int> components; // 1-based
std::vector<double> ratios;
std::vector<long long> accum; // deficit accumulator (integer, unit: 1e-6 mm)
};
std::vector<SlotInfo> slots(is_mixed.size());
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
slots[i].components = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
if (slots[i].components.size() < 2) {
slots[i].components.clear();
continue;
}
for (unsigned int cid : slots[i].components) {
unsigned int idx0 = cid - 1;
if (idx0 >= is_mixed.size() || (idx0 < is_mixed.size() && is_mixed[idx0])) {
slots[i].components.clear();
break;
}
}
if (slots[i].components.empty())
continue;
slots[i].ratios = parse_mixed_ratios(
i < ratio_strs.size() ? ratio_strs[i] : "", slots[i].components.size());
if (!sublayer_enabled) {
for (double &r : slots[i].ratios)
r = snap_to_simple_fraction(r);
double sum = 0;
for (double r : slots[i].ratios) sum += r;
if (sum > 0)
for (double &r : slots[i].ratios) r /= sum;
}
slots[i].accum.assign(slots[i].components.size(), 0LL);
}
// Parse gradient settings per slot
const auto &gradient_flags = config.filament_mixed_gradient.values;
const auto &gradient_range_strs = config.filament_mixed_gradient_range.values;
const auto &gradient_curve_strs = config.filament_mixed_gradient_curve.values;
struct GradientInfo {
double start = 0.10;
double end_val = 0.90;
GradientCurve curve; // empty -> use linear (start, end_val); non-empty wins
};
std::vector<bool> is_gradient(is_mixed.size(), false);
std::vector<GradientInfo> gradient_info(is_mixed.size());
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i] || slots[i].components.size() != 2)
continue;
if (i >= gradient_flags.size() || !gradient_flags[i])
continue;
is_gradient[i] = true;
if (i < gradient_range_strs.size() && !gradient_range_strs[i].empty()) {
CNumericLocalesSetter c_locale_setter;
float v0 = 0, v1 = 0;
if (std::sscanf(gradient_range_strs[i].c_str(), "%f,%f", &v0, &v1) == 2 &&
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
gradient_info[i].start = v0;
gradient_info[i].end_val = v1;
}
}
if (i < gradient_curve_strs.size() && !gradient_curve_strs[i].empty())
gradient_info[i].curve = parse_gradient_curve(gradient_curve_strs[i]);
}
// Pass 1: identify continuous runs for each gradient slot (Per-Run).
// A "run" is a maximal sequence of consecutive layers where the slot appears.
struct GradientRunInfo {
std::vector<size_t> run_lengths;
int current_run = -1;
size_t current_idx = 0;
bool prev_appeared = false;
bool last_absent_was_relevant = false;
};
std::map<unsigned int, GradientRunInfo> gradient_runs;
for (size_t i = 0; i < is_mixed.size(); ++i)
if (is_gradient[i]) gradient_runs[static_cast<unsigned int>(i)] = {};
// Build per-slot sets of all layer indices where any slot-owning object has a
// layer. Used by gradient run detection (a gap is real only if the slot is
// absent at a layer belonging to one of its own objects) and by calc_slot_lh
// to keep prev_relevant_z_for_slot current even when a slot skips many layers.
std::map<unsigned int, std::set<size_t>> slot_relevant_layers;
for (auto &[slot_idx, obj_map] : m_mixed_object_layers) {
for (auto &[obj, _] : obj_map) {
auto it = m_object_all_layer_indices.find(obj);
if (it != m_object_all_layer_indices.end())
slot_relevant_layers[slot_idx].insert(it->second.begin(), it->second.end());
}
}
if (!gradient_runs.empty()) {
for (size_t li = 0; li < m_layer_tools.size(); ++li) {
if (li == 0) continue;
const auto &lt = m_layer_tools[li];
for (auto &[slot, run] : gradient_runs) {
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
if (here) {
bool real_gap = false;
if (!run.prev_appeared && !run.run_lengths.empty()) {
real_gap = run.last_absent_was_relevant;
}
if (run.run_lengths.empty() || real_gap)
run.run_lengths.push_back(0);
run.run_lengths.back()++;
run.last_absent_was_relevant = false;
} else if (!run.run_lengths.empty()) {
auto rel_it = slot_relevant_layers.find(slot);
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(li))
run.last_absent_was_relevant = true;
}
run.prev_appeared = here;
}
}
for (auto &[slot, run] : gradient_runs) {
run.current_run = -1;
run.current_idx = 0;
run.prev_appeared = false;
run.last_absent_was_relevant = false;
}
}
// Per-object gradient: pre-compute per-object runs (respecting Z gaps within each object).
struct PerObjRunState {
std::vector<size_t> run_start_offsets; // index into layer_indices where each run starts
std::vector<size_t> run_lengths;
int current_run = -1;
size_t current_idx = 0;
};
// Detect whether a gap between two consecutive gradient-slot appearances is a
// real run break. A gap is real only if the object has its own layer inside the
// gap that does NOT use the gradient slot (i.e. the slot was genuinely absent).
// Uses lower_bound to skip global indices that don't belong to the object.
auto has_real_gap = [](size_t prev_idx, size_t cur_idx,
const std::set<size_t>& obj_set,
const std::set<size_t>& slot_set) -> bool {
for (auto it = obj_set.lower_bound(prev_idx + 1);
it != obj_set.end() && *it < cur_idx; ++it) {
if (!slot_set.count(*it))
return true;
}
return false;
};
// Segment a sorted list of layer indices into runs, using has_real_gap to decide
// where to break. Shared by the per-object and per-volume paths below.
auto segment_runs = [&](const std::vector<size_t>& layer_indices,
const std::set<size_t>& obj_set,
const std::set<size_t>& slot_set) -> PerObjRunState {
PerObjRunState st;
for (size_t i = 0; i < layer_indices.size(); ++i) {
bool new_run = (i == 0) ||
has_real_gap(layer_indices[i - 1], layer_indices[i], obj_set, slot_set);
if (new_run) {
st.run_start_offsets.push_back(i);
st.run_lengths.push_back(0);
}
st.run_lengths.back()++;
}
return st;
};
std::map<unsigned int, std::map<const PrintObject*, PerObjRunState>> per_obj_runs;
for (auto &[slot, obj_map] : m_mixed_object_layers) {
if (slot >= is_gradient.size() || !is_gradient[slot])
continue;
for (auto &[obj, layer_indices] : obj_map) {
sort_remove_duplicates(layer_indices);
// Erase layer 0 — this mutation is also relied upon by the Pass 2 binary_search below.
if (!layer_indices.empty() && layer_indices.front() == 0)
layer_indices.erase(layer_indices.begin());
const auto &all_obj_layers = m_object_all_layer_indices[obj];
std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end());
std::set<size_t> grad_set(layer_indices.begin(), layer_indices.end());
per_obj_runs[slot][obj] = segment_runs(layer_indices, all_obj_set, grad_set);
}
}
// Per-volume gradient: mirror the per-object run-segmentation logic above for
// m_gradient_volume_layers. When per_part_gradient is off (or no qualifying volume exists),
// m_gradient_volume_layers is empty and per_vol_runs ends up empty too — so all subsequent
// checks of `per_vol_runs.find(slot) != end()` will fail and the legacy per-object path
// remains the only path taken.
using VolumeKey = LayerTools::MixedSubLayerGroup::VolumeKey;
std::map<unsigned int, std::map<VolumeKey, PerObjRunState>> per_vol_runs;
for (auto &[slot, vol_map] : m_gradient_volume_layers) {
if (slot >= is_gradient.size() || !is_gradient[slot])
continue;
for (auto &[vkey, layer_indices] : vol_map) {
sort_remove_duplicates(layer_indices);
if (!layer_indices.empty() && layer_indices.front() == 0)
layer_indices.erase(layer_indices.begin());
const auto &all_obj_layers = m_object_all_layer_indices[vkey.obj];
std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end());
std::set<size_t> vol_grad_set(layer_indices.begin(), layer_indices.end());
per_vol_runs[slot][vkey] = segment_runs(layer_indices, all_obj_set, vol_grad_set);
}
}
// Pass 2: resolve per layer
coordf_t prev_print_z = 0.;
// Track last print_z per mixed slot so that layer height is computed from the
// slot's own previous appearance, not from a global Z that may include layers
// belonging only to other objects with different layer heights.
std::map<unsigned int, coordf_t> prev_print_z_for_slot;
// Track the last Z where a slot-owning object had ANY layer (regardless of
// whether the slot was present). Used to detect genuine gaps: if the slot was
// absent but its owner objects had layers, prev_relevant_z advances while
// prev_print_z_for_slot stays stale. Taking the max of both gives correct lh.
std::map<unsigned int, coordf_t> prev_relevant_z_for_slot;
// Compute the effective layer height for a mixed slot by choosing the best
// reference Z among: (1) the slot's own last Z, (2) the last Z where the
// slot's owning object had any layer, (3) the global previous Z as fallback
// when the slot appears for the first time.
auto calc_slot_lh = [&](unsigned int ext, coordf_t print_z) -> double {
auto slot_pz_it = prev_print_z_for_slot.find(ext);
auto rel_pz_it = prev_relevant_z_for_slot.find(ext);
coordf_t base_z = prev_print_z;
if (slot_pz_it != prev_print_z_for_slot.end()) {
base_z = slot_pz_it->second;
if (rel_pz_it != prev_relevant_z_for_slot.end())
base_z = std::max(base_z, rel_pz_it->second);
}
double lh = print_z - base_z;
return (lh > 0.) ? lh : 0.2; // 0.2mm safety fallback; should not trigger in normal operation
};
for (LayerTools &lt : m_layer_tools) {
size_t layer_idx = static_cast<size_t>(&lt - m_layer_tools.data());
// Update gradient run state (skip first layer to match counting).
if (layer_idx > 0) {
for (auto &[slot, run] : gradient_runs) {
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
if (here) {
if (!run.prev_appeared) {
if (run.last_absent_was_relevant || run.current_run < 0) {
run.current_run++;
run.current_idx = 0;
}
}
run.last_absent_was_relevant = false;
} else {
auto rel_it = slot_relevant_layers.find(slot);
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(layer_idx))
run.last_absent_was_relevant = true;
}
run.prev_appeared = here;
}
}
std::vector<unsigned int> new_extruders;
for (unsigned int ext : lt.extruders) {
if (ext >= slots.size() || slots[ext].components.empty()) {
new_extruders.push_back(ext);
continue;
}
auto &s = slots[ext];
// Skip sublayer splitting for the first layer to preserve bed adhesion.
if (sublayer_enabled && layer_idx > 0) {
double lh = calc_slot_lh(ext, lt.print_z);
size_t n = s.components.size();
std::vector<double> sub_heights;
bool gradient_last_no_split = false;
unsigned int gradient_last_dominant_0b = 0;
if (is_gradient[ext] && n == 2) {
auto gr_it = gradient_runs.find(ext);
if (gr_it != gradient_runs.end() && gr_it->second.current_run >= 0 &&
static_cast<size_t>(gr_it->second.current_run) < gr_it->second.run_lengths.size()) {
auto &run = gr_it->second;
size_t N = run.run_lengths[run.current_run];
size_t idx = run.current_idx++;
double t = (N > 0) ? (2.0 * idx + 1.0) / (2.0 * N) : 0.5;
// Custom curve wins over linear range when present; OFF path stays bit-identical.
double r1 = gradient_info[ext].curve.empty()
? (gradient_info[ext].start + (gradient_info[ext].end_val - gradient_info[ext].start) * t)
: sample_gradient_curve(gradient_info[ext].curve, t);
double r2 = 1.0 - r1;
sub_heights.push_back(r1 * lh);
sub_heights.push_back(r2 * lh);
// The sublayer split path sorts components by physical ID ascending;
// the higher-ID component ends up on top (visible surface). If the
// gradient's dominant component has the lower physical ID, splitting
// would put the non-dominant color on the visible top surface. In
// that case, skip the split and print this final run-layer as pure
// dominant color to preserve the gradient appearance.
if (idx == N - 1) {
// When r1 == r2 (exactly 50/50), component[0] is treated as dominant.
size_t dominant = (r1 >= r2) ? 0 : 1;
unsigned int dom_0b = s.components[dominant] - 1;
unsigned int oth_0b = s.components[1 - dominant] - 1;
if (dom_0b < oth_0b) {
gradient_last_no_split = true;
gradient_last_dominant_0b = dom_0b;
}
}
} else {
for (double r : s.ratios)
sub_heights.push_back(r * lh);
}
} else {
for (double r : s.ratios)
sub_heights.push_back(r * lh);
}
// Per-part gradient: when this slot has any qualifying volume, the global
// no-split short-circuit must NOT bypass MixedSubLayerGroup creation — each
// volume needs its own no-split decision in GCode.cpp (a per-volume "last
// run-layer" can occur on a different layer index than the per-object one). We
// still keep the per-object short-circuit when per_vol_runs[ext] is empty, which
// covers the legacy path bit-identically.
bool per_vol_active_for_slot = per_vol_runs.find(ext) != per_vol_runs.end()
&& !per_vol_runs[ext].empty();
if (gradient_last_no_split && !per_vol_active_for_slot) {
lt.mixed_filament_resolution[ext] = gradient_last_dominant_0b;
new_extruders.push_back(gradient_last_dominant_0b);
prev_print_z_for_slot[ext] = lt.print_z;
continue;
}
LayerTools::MixedSubLayerGroup grp;
grp.mixed_slot_0based = ext;
grp.layer_height = lh;
grp.is_gradient = is_gradient[ext];
for (size_t k = 0; k < s.components.size(); ++k) {
unsigned int comp_0based = s.components[k] - 1;
grp.components_0based.push_back(comp_0based);
}
grp.sub_heights = sub_heights;
// Write gradient metadata (run-aware). Both per_object_gradient and
// per_volume_gradient are populated independently from their own run-state
// machines; the GCode emitter chooses per-region:
// - tagged region (gradient_volume_id valid) -> per_volume_gradient[{obj, vol}]
// - untagged region (modifier / painted / etc.) -> per_object_gradient[obj]
// Populating both keeps the per-object run state correct even when per-volume
// takes over for the same (slot, obj), and lets untagged geometry (which is
// never split per-volume) keep its per-object gradient ratios.
if (grp.is_gradient) {
auto vol_runs_slot_it = per_vol_runs.find(ext);
if (vol_runs_slot_it != per_vol_runs.end()) {
auto vol_slot_it = m_gradient_volume_layers.find(ext);
for (auto &[vkey, st] : vol_runs_slot_it->second) {
auto &layer_indices = vol_slot_it->second[vkey];
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
continue;
if (st.current_run < 0 ||
st.current_idx >= st.run_lengths[st.current_run]) {
st.current_run++;
st.current_idx = 0;
}
size_t run_N = st.run_lengths[st.current_run];
size_t run_idx = st.current_idx++;
grp.per_volume_gradient[vkey] = {
run_N,
run_idx,
gradient_info[ext].start,
gradient_info[ext].end_val,
gradient_info[ext].curve,
};
}
}
auto runs_slot_it = per_obj_runs.find(ext);
if (runs_slot_it != per_obj_runs.end()) {
auto slot_it = m_mixed_object_layers.find(ext);
for (auto &[obj, st] : runs_slot_it->second) {
auto &layer_indices = slot_it->second[obj];
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
continue;
if (st.current_run < 0 ||
st.current_idx >= st.run_lengths[st.current_run]) {
st.current_run++;
st.current_idx = 0;
}
size_t run_N = st.run_lengths[st.current_run];
size_t run_idx = st.current_idx++;
grp.per_object_gradient[obj] = {
run_N,
run_idx,
gradient_info[ext].start,
gradient_info[ext].end_val,
gradient_info[ext].curve,
};
}
}
}
if (grp.components_0based.size() > 1) {
unsigned int first_comp_0based = s.components[0] - 1;
std::vector<size_t> idx(grp.components_0based.size());
std::iota(idx.begin(), idx.end(), 0);
std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b) {
return grp.components_0based[a] < grp.components_0based[b];
});
std::vector<unsigned int> sorted_comps;
std::vector<double> sorted_heights;
for (size_t i : idx) {
sorted_comps.push_back(grp.components_0based[i]);
sorted_heights.push_back(grp.sub_heights[i]);
}
grp.components_0based = std::move(sorted_comps);
grp.sub_heights = std::move(sorted_heights);
if (grp.is_gradient) {
for (size_t i = 0; i < grp.components_0based.size(); ++i) {
if (grp.components_0based[i] == first_comp_0based) {
grp.gradient_first_sorted_idx = static_cast<int>(i);
break;
}
}
}
}
for (unsigned int comp : grp.components_0based)
new_extruders.push_back(comp);
lt.mixed_sub_layer_groups.push_back(std::move(grp));
prev_print_z_for_slot[ext] = lt.print_z;
} else {
// Deficit Round-Robin: pick one component per layer.
// Weight by layer height so volume ratios stay accurate
// even with adaptive layer heights.
double lh = calc_slot_lh(ext, lt.print_z);
long long lh_i = std::llround(lh * 1e6);
// For 2-component gradient on the first layer, use the gradient's
// starting ratio instead of the configured mixing ratio so the
// selected filament matches the gradient's "from" end.
// Only affects the first layer; when sublayer splitting is enabled
// (required for gradient), layers 1+ take the sublayer path and
// do not touch the DRR accumulator.
if (layer_idx == 0 && is_gradient[ext] && s.components.size() == 2) {
double r0 = gradient_info[ext].start;
s.accum[0] += std::llround(r0 * lh_i);
s.accum[1] += std::llround((1.0 - r0) * lh_i);
} else {
for (size_t k = 0; k < s.ratios.size(); ++k)
s.accum[k] += std::llround(s.ratios[k] * lh_i);
}
size_t sel = 0;
for (size_t k = 1; k < s.accum.size(); ++k)
if (s.accum[k] > s.accum[sel])
sel = k;
s.accum[sel] -= lh_i;
unsigned int resolved = s.components[sel] - 1;
lt.mixed_filament_resolution[ext] = resolved;
new_extruders.push_back(resolved);
prev_print_z_for_slot[ext] = lt.print_z;
}
}
lt.extruders = new_extruders;
sort_remove_duplicates(lt.extruders);
// Update prev_relevant_z: for each slot that has relevant-layer tracking,
// advance if the current layer belongs to a slot-owning object.
for (auto &[slot, rel_set] : slot_relevant_layers) {
if (rel_set.count(layer_idx))
prev_relevant_z_for_slot[slot] = lt.print_z;
}
prev_print_z = lt.print_z;
}
}
void ToolOrdering::enforce_mixed_component_order()
{
for (LayerTools &lt : m_layer_tools) {
if (lt.mixed_sub_layer_groups.empty())
continue;
// Build a set of extruders present in lt.extruders for fast lookup.
std::set<unsigned int> ext_set(lt.extruders.begin(), lt.extruders.end());
// 1. Build DAG from mixed group constraints.
// For each group [c0, c1, c2, ...], add edges c0->c1, c1->c2, ...
// Only between components that are both present in lt.extruders.
// Use an edge set to avoid duplicate edges inflating in-degree.
std::map<unsigned int, std::vector<unsigned int>> adj;
std::map<unsigned int, int> in_degree;
std::set<std::pair<unsigned int, unsigned int>> edge_set;
for (unsigned int ext : lt.extruders)
in_degree[ext] = 0;
for (const auto &grp : lt.mixed_sub_layer_groups) {
for (size_t i = 0; i + 1 < grp.components_0based.size(); ++i) {
unsigned int a = grp.components_0based[i];
unsigned int b = grp.components_0based[i + 1];
if (!ext_set.count(a) || !ext_set.count(b))
continue;
if (edge_set.insert({a, b}).second) {
adj[a].push_back(b);
in_degree[b] += 1;
}
}
}
// 2. Record original position (from flush optimizer) as priority.
std::map<unsigned int, size_t> orig_pos;
for (size_t i = 0; i < lt.extruders.size(); ++i)
orig_pos[lt.extruders[i]] = i;
// 3. Kahn's topological sort with priority queue (prefer original position).
auto cmp = [&orig_pos](unsigned int lhs, unsigned int rhs) {
return orig_pos[lhs] > orig_pos[rhs]; // min-heap by orig_pos
};
std::priority_queue<unsigned int, std::vector<unsigned int>, decltype(cmp)> pq(cmp);
for (unsigned int ext : lt.extruders) {
if (in_degree[ext] == 0)
pq.push(ext);
}
std::vector<unsigned int> ordered;
ordered.reserve(lt.extruders.size());
while (!pq.empty()) {
unsigned int ext = pq.top();
pq.pop();
ordered.push_back(ext);
if (auto it = adj.find(ext); it != adj.end()) {
for (unsigned int next : it->second) {
if (--in_degree[next] == 0)
pq.push(next);
}
}
}
// Safety: if topological sort didn't produce all elements, keep original order.
if (ordered.size() != lt.extruders.size())
ordered = lt.extruders;
// 4. Verify: every mixed group's component order is preserved as subsequence.
for (const auto &grp : lt.mixed_sub_layer_groups) {
size_t prev_pos = 0;
bool valid = true;
for (unsigned int c : grp.components_0based) {
if (!ext_set.count(c))
continue;
auto it = std::find(ordered.begin() + prev_pos, ordered.end(), c);
if (it == ordered.end()) { valid = false; break; }
prev_pos = (it - ordered.begin()) + 1;
}
assert(valid && "enforce_mixed_component_order: mixed group subsequence violated");
(void)valid;
}
lt.extruders = ordered;
}
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -1998,6 +2785,17 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
std::vector<unsigned int> used_filaments = collect_sorted_used_filaments(layer_filaments);
std::vector<std::set<int>>geometric_unprintables = m_print->get_geometric_unprintable_filaments();
// Unprintable sets are keyed by filament id, but a mixed-color slot is virtual: what actually
// reaches the nozzle are its components. Expand the slot to those components so a geometric
// restriction is applied to the filaments really being printed. No-op without mixed filaments.
{
const auto &is_mixed = m_print->config().filament_is_mixed.values;
const auto &comp_strs = m_print->config().filament_mixed_components.values;
if (has_any_mixed_filament(is_mixed))
expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs);
}
std::vector<std::set<int>>physical_unprintables = m_print->get_physical_unprintable_filaments(used_filaments);
auto filament_unprintable_volumes = m_print->get_filament_unprintable_flow(used_filaments);
+86
View File
@@ -5,12 +5,16 @@
#include "../libslic3r.h"
#include <functional>
#include <map>
#include <utility>
#include <boost/container/small_vector.hpp>
#include "../FilamentGroup.hpp"
#include "../FilamentMixer.hpp"
#include "../MultiNozzleUtils.hpp"
#include "../ExtrusionEntity.hpp"
#include "../ObjectID.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
@@ -172,6 +176,65 @@ public:
// Custom G-code (color change, extruder switch, pause) to be performed before this layer starts to print.
const CustomGCode::Item *custom_gcode = nullptr;
// 0-based mixed filament slot → 0-based resolved physical filament for this layer.
// Populated by ToolOrdering::resolve_mixed_filaments(). Empty when no mixed filaments.
std::map<unsigned int, unsigned int> mixed_filament_resolution;
unsigned int resolve_mixed(unsigned int filament_0based) const {
auto it = mixed_filament_resolution.find(filament_0based);
return (it != mixed_filament_resolution.end()) ? it->second : filament_0based;
}
struct MixedSubLayerGroup {
unsigned int mixed_slot_0based;
std::vector<unsigned int> components_0based;
std::vector<double> sub_heights; // per-component, sum ≈ layer_height
double layer_height = 0.; // the actual lh used to compute sub_heights
bool is_gradient = false;
int gradient_first_sorted_idx = 0; // index of "first" config component after sorting
struct ObjectGradient {
size_t total_layers;
size_t current_idx;
double gradient_start;
double gradient_end;
GradientCurve curve; // empty -> linear fallback (start, end); non-empty wins
};
std::map<const PrintObject*, ObjectGradient> per_object_gradient;
// Per-volume gradient: same metadata layout as ObjectGradient but keyed by
// (PrintObject*, ModelVolume id). Populated only when filament_mixed_gradient_per_part is
// enabled for this slot AND the corresponding ModelObject contains >=2 model-part volumes
// using this slot. When non-empty for a given (PrintObject*), GCode emission takes the
// per-volume path for tagged regions; untagged regions (modifier/painted/fuzzy_skin) still
// use per_object_gradient. Both maps are populated in parallel to keep run states correct.
struct VolumeKey {
const PrintObject* obj;
ObjectID volume_id;
bool operator<(const VolumeKey &o) const {
if (obj != o.obj) return std::less<const PrintObject*>{}(obj, o.obj);
return volume_id < o.volume_id;
}
bool operator==(const VolumeKey &o) const {
return obj == o.obj && volume_id == o.volume_id;
}
};
using VolumeGradient = ObjectGradient;
std::map<VolumeKey, VolumeGradient> per_volume_gradient;
};
std::vector<MixedSubLayerGroup> mixed_sub_layer_groups;
const MixedSubLayerGroup* mixed_group_by_slot(unsigned int slot_id) const {
for (const auto &g : mixed_sub_layer_groups)
if (g.mixed_slot_0based == slot_id)
return &g;
return nullptr;
}
bool is_mixed_slot(unsigned int slot_id) const {
return mixed_group_by_slot(slot_id) != nullptr;
}
WipingExtrusions& wiping_extrusions() {
m_wiping_extrusions.set_layer_tools_ptr(this);
return m_wiping_extrusions;
@@ -227,6 +290,9 @@ public:
// For a multi-material print, the printing extruders are ordered in the order they shall be primed.
const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; }
// 0-based mixed (virtual) slots that appeared on layers before resolve_mixed_filaments
// expanded them to physical components.
const std::vector<unsigned int>& used_mixed_filaments() const { return m_used_mixed_filaments; }
// Find LayerTools with the closest print_z.
const LayerTools& tools_for_layer(coordf_t print_z) const;
@@ -299,6 +365,8 @@ private:
void mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height);
void collect_extruder_statistics(bool prime_multi_material);
void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer);
void resolve_mixed_filaments(const PrintConfig &config);
void enforce_mixed_component_order();
// BBS
std::vector<unsigned int> generate_first_layer_tool_order(const Print& print);
@@ -311,8 +379,26 @@ private:
unsigned int m_last_printing_extruder = (unsigned int)-1;
// All extruders, which extrude some material over m_layer_tools.
std::vector<unsigned int> m_all_printing_extruders;
std::vector<unsigned int> m_used_mixed_filaments;
const DynamicPrintConfig* m_print_full_config = nullptr;
const PrintConfig* m_print_config_ptr = nullptr;
// Per-object gradient tracking: slot(0-based) -> PrintObject* -> list of layer indices
// where that object uses the slot. Populated by collect_extruders, consumed by resolve_mixed_filaments.
std::map<unsigned int, std::map<const PrintObject*, std::vector<size_t>>> m_mixed_object_layers;
// All layer indices (in m_layer_tools) where each object has any layer.
// Used by gradient run detection to distinguish real gaps (object has a layer
// that doesn't use the slot) from spurious gaps (another object's layer).
std::map<const PrintObject*, std::vector<size_t>> m_object_all_layer_indices;
// Per-volume gradient tracking: slot(0-based) -> (PrintObject*, ModelVolume id) -> list of
// layer indices where the given volume contributes to the slot. Populated by collect_extruders
// alongside m_mixed_object_layers when per_part gradient is enabled for the slot AND the
// ModelObject has >=2 model-part volumes using the slot. Empty for all other configurations,
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
// no-ops; downstream emission falls through to the per-object branch).
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
bool m_sorted = false;
+6
View File
@@ -210,6 +210,12 @@ void Layer::make_perimeters()
if (! (*it)->slices.empty()) {
LayerRegion* other_layerm = *it;
const PrintRegion &other_region = other_layerm->region();
// Per-part gradient tags a region with its owning ModelVolume; merging two
// differently-tagged regions would collapse volumes that need independent
// gradient runs. Both tags are invalid unless per-part gradient is on, so
// this is a no-op for every other configuration.
if (this_region.gradient_volume_id() != other_region.gradient_volume_id())
continue;
if (is_perimeter_compatible(*m_object->print(), this_region, other_region))
{
other_layerm->perimeters.clear();
+1 -1
View File
@@ -53,7 +53,7 @@ bool is_decimal_separator_point()
double string_to_double_decimal_point(const std::string_view str, size_t* pos /* = nullptr*/)
{
double out;
double out = 0.;
size_t p = fast_float::from_chars(str.data(), str.data() + str.size(), out).ptr - str.data();
if (pos)
*pos = p;
+129 -26
View File
@@ -1,6 +1,8 @@
#include "Model.hpp"
#include "libslic3r.h"
#include "BuildVolume.hpp"
#include "TexturePainting.hpp"
#include "Format/AssimpImport.hpp"
#include "ClipperUtils.hpp"
#include "Exception.hpp"
#include "Model.hpp"
@@ -104,6 +106,7 @@ Model& Model::assign_copy(const Model &rhs)
this->mk_version = rhs.mk_version;
this->md_name = rhs.md_name;
this->md_value = rhs.md_value;
this->texture_mesh = rhs.texture_mesh;
return *this;
}
@@ -139,6 +142,7 @@ Model& Model::assign_copy(Model &&rhs)
this->mk_version = rhs.mk_version;
this->md_name = rhs.md_name;
this->md_value = rhs.md_value;
this->texture_mesh = std::move(rhs.texture_mesh);
this->backup_path = std::move(rhs.backup_path);
this->object_backup_id_map = std::move(rhs.object_backup_id_map);
this->next_object_backup_id = rhs.next_object_backup_id;
@@ -239,6 +243,27 @@ _finished:
// BBS: add part plate related logic
// BBS: backup & restore
// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well.
// Build a plain geometry ModelObject from a textured mesh. The texture itself is carried
// separately on Model::texture_mesh and consumed by the texture import dialog.
static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mesh, const std::string& input_file)
{
std::string object_name = boost::filesystem::path(input_file).filename().string();
indexed_triangle_set its;
its.vertices.resize(tex_mesh.vertices.size());
for (size_t i = 0; i < tex_mesh.vertices.size(); ++i)
its.vertices[i] = Vec3f(tex_mesh.vertices[i][0], tex_mesh.vertices[i][1], tex_mesh.vertices[i][2]);
its.indices.resize(tex_mesh.indices.size());
for (size_t i = 0; i < tex_mesh.indices.size(); ++i)
its.indices[i] = Vec3i32(tex_mesh.indices[i][0], tex_mesh.indices[i][1], tex_mesh.indices[i][2]);
its_merge_vertices(its);
its_remove_degenerate_faces(its);
its_compactify_vertices(its);
model.add_object(object_name.c_str(), input_file.c_str(), std::move(TriangleMesh(std::move(its))));
}
Model Model::read_from_file(const std::string& input_file,
DynamicPrintConfig* config,
ConfigSubstitutionContext* config_substitutions,
@@ -281,32 +306,85 @@ Model Model::read_from_file(const std::string&
result = load_stl(input_file.c_str(), &model, nullptr, stlFn,256);
else if (boost::algorithm::iends_with(input_file, ".obj")) {
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;
ObjParser::MtlData mtl_data;
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
// instead of the flat per-face colour dialog.
auto tex_mesh = std::make_shared<TexturedMesh>();
std::string obj_dir = boost::filesystem::path(input_file).parent_path().string();
if (obj_to_textured_mesh(obj_info,
model.objects.back()->volumes[0]->mesh().its,
mtl_data, obj_dir, *tex_mesh)) {
model.texture_mesh = tex_mesh;
}
}
else if (result && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
// Vertex-colour and MTL face-colour OBJs also go through the texture-to-color
// importer (as precomputed per-face colors) instead of the flat
// per-face colour dialog, matching the uv_png branch above.
auto build_tex_mesh_geometry = [&]() {
auto tex_mesh = std::make_shared<TexturedMesh>();
const auto& its = model.objects.back()->volumes[0]->mesh().its;
tex_mesh->vertices.resize(its.vertices.size());
for (size_t i = 0; i < its.vertices.size(); ++i)
tex_mesh->vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
tex_mesh->indices.resize(its.indices.size());
for (size_t i = 0; i < its.indices.size(); ++i)
tex_mesh->indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
return tex_mesh;
};
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);
in_out.is_single_color = false;
in_out.deal_vertex_color = true;
objFn(in_out);
auto tex_mesh = build_tex_mesh_geometry();
const auto& its = model.objects.back()->volumes[0]->mesh().its;
tex_mesh->precomputed_face_colors.resize(its.indices.size());
for (size_t i = 0; i < its.indices.size(); ++i) {
const auto& f = its.indices[i];
auto avg = [&](int ch) -> std::size_t {
float v = (obj_info.vertex_colors[f[0]][ch]
+ obj_info.vertex_colors[f[1]][ch]
+ obj_info.vertex_colors[f[2]][ch]) / 3.0f * 255.0f;
return (std::size_t) std::clamp(v, 0.0f, 255.0f);
};
tex_mesh->precomputed_face_colors[i] = {avg(0), avg(1), avg(2)};
}
} 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);
in_out.is_single_color = obj_info.is_single_mtl;
in_out.deal_vertex_color = false;
objFn(in_out);
tex_mesh->precomputed_vertex_colors = obj_info.vertex_colors;
model.texture_mesh = tex_mesh;
} else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) {
auto tex_mesh = build_tex_mesh_geometry();
const size_t nf = tex_mesh->indices.size();
tex_mesh->precomputed_face_colors.resize(nf);
for (size_t i = 0; i < nf; ++i) {
if (i < obj_info.face_colors.size()) {
const auto& c = obj_info.face_colors[i];
tex_mesh->precomputed_face_colors[i] = {
(std::size_t) std::clamp(c[0] * 255.0f, 0.0f, 255.0f),
(std::size_t) std::clamp(c[1] * 255.0f, 0.0f, 255.0f),
(std::size_t) std::clamp(c[2] * 255.0f, 0.0f, 255.0f)
};
} else {
tex_mesh->precomputed_face_colors[i] = {128, 128, 128};
}
}
} /*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.");
}*/
model.texture_mesh = tex_mesh;
}
}
}
else if (boost::algorithm::iends_with(input_file, ".glb") ||
boost::algorithm::iends_with(input_file, ".gltf") ||
boost::algorithm::iends_with(input_file, ".fbx")) {
// These formats can carry material/texture data, so they go through the textured
// import path: the geometry becomes a normal object and the texture is handed to the
// texture-to-color dialog via Model::texture_mesh.
auto tex_mesh = std::make_shared<TexturedMesh>();
result = load_assimp_textured_model(input_file, *tex_mesh, &message);
if (result) {
model.texture_mesh = tex_mesh;
add_textured_mesh_to_model(model, *tex_mesh, input_file);
} else if (!message.empty()) {
BOOST_LOG_TRIVIAL(error) << "Assimp: failed to load model: " << message
<< ", path=" << input_file;
message = _L("The file format is incompatible and cannot be parsed.");
}
}
else if (boost::algorithm::iends_with(input_file, ".svg"))
@@ -578,6 +656,7 @@ void Model::clear_objects()
this->objects.clear();
object_backup_id_map.clear();
next_object_backup_id = 1;
texture_mesh.reset();
}
// BBS: backup, reuse objects
@@ -2576,7 +2655,8 @@ void ModelVolume::update_extruder_count(size_t extruder_count)
}
}
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id)
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id,
const std::vector<unsigned char> &filament_is_mixed)
{
std::vector<int> used_extruders = get_extruders();
for (int extruder_id : used_extruders) {
@@ -2587,8 +2667,22 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou
}
// Same stale-assignment cleanup as update_extruder_count, for the filament-delete path.
// Ported from BambuStudio (STUDIO-15763).
if (extruder_id() > extruder_count) {
this->config.erase("extruder");
size_t eid = extruder_id();
// Judge out-of-range against the post-remap id, mirroring update_filament_values_for_items_when_delete_filament.
// Using the pre-remap eid would wrongly erase a high extruder that should remap (e.g. 5 -> 4 after
// deleting filament 1); update_filament_values_for_items_when_delete_filament would then skip it
// (!has("extruder")) and the volume would fall back to the object default color.
size_t remapped = eid;
if (eid == filament_id)
remapped = (replace_filament_id > 0) ? (size_t)replace_filament_id : 1;
else if (eid > filament_id)
remapped = eid - 1;
if (remapped > extruder_count) {
// filament_is_mixed is the pre-delete snapshot; index it with the ORIGINAL eid (1-based),
// not remapped, so we check whether this volume's current slot is a mixed slot.
bool is_mixed = !filament_is_mixed.empty() && eid >= 1 && (eid - 1) < filament_is_mixed.size() && filament_is_mixed[eid - 1];
if (!is_mixed)
this->config.erase("extruder");
}
}
@@ -3495,6 +3589,15 @@ void FacetsAnnotation::get_facets(const ModelVolume& mv, std::vector<indexed_tri
selector.get_facets(facets_per_type);
}
void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta)
{
if (empty()) return;
TriangleSelector selector(mv.mesh());
selector.deserialize(m_data, false);
selector.shift_states_above(threshold, delta);
this->set(selector);
}
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
EnforcerBlockerType max_type,
EnforcerBlockerType to_delete_filament,
+11 -1
View File
@@ -47,6 +47,8 @@ namespace cereal {
}
namespace Slic3r {
struct TexturedMesh;
enum class ConversionType;
class BuildVolume;
@@ -740,6 +742,9 @@ public:
EnforcerBlockerType max_type,
EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE,
EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE);
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
bool empty() const { return m_data.triangles_to_split.empty(); }
@@ -932,7 +937,8 @@ public:
// BBS
std::vector<int> get_extruders() const;
void update_extruder_count(size_t extruder_count);
void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1);
void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1,
const std::vector<unsigned char> &filament_is_mixed = {});
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
@@ -1549,6 +1555,10 @@ public:
std::shared_ptr<ModelInfo> model_info = nullptr;
std::shared_ptr<ModelProfileInfo> profile_info = nullptr;
// Textured mesh data for texture-to-painting import. Populated by the loader when a mesh
// arrives with usable UVs and a texture map; consumed (and reset) by the import dialog.
std::shared_ptr<TexturedMesh> texture_mesh;
//makerlab information
std::string mk_name;
std::string mk_version;
+2
View File
@@ -1,4 +1,6 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "OpenVDBUtils.hpp"
#ifdef _MSC_VER
+16
View File
@@ -229,6 +229,22 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
// Append thin walls to the nearest-neighbor search (only for first iteration)
if (! thin_walls.empty()) {
// Orca: apply fuzzy skin to thin walls as well
for (auto& thin_wall : thin_walls) {
// First, we convert the ThickPolyline into Arachne::ExtrusionLine so we could reuse our existing fuzzy code
Arachne::ExtrusionLine el(0, true);
el.junctions.reserve(thin_wall.points.size());
for (int i = 0; i < thin_wall.points.size(); i++) {
el.junctions.emplace_back(thin_wall.points[i], thin_wall.width[i], 0);
}
// Then we fuzzy it
apply_fuzzy_skin(&el, perimeter_generator, true, thin_wall.is_closed());
// Then convert the result back to ThickPolyline
thin_wall = Arachne::to_thick_polyline(el);
}
variable_width(thin_walls, erExternalPerimeter, perimeter_generator.ext_perimeter_flow, coll.entities);
thin_walls.clear();
}
+3
View File
@@ -8,7 +8,9 @@
#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif /* _MSC_VER */
@@ -1189,6 +1191,7 @@ static std::vector<std::string> s_Preset_print_options{
"flush_into_infill",
"flush_into_objects",
"flush_into_support",
"enable_mixed_color_sublayer",
"tree_support_branch_angle",
"tree_support_angle_slow",
"tree_support_wall_count",
+285 -2
View File
@@ -7,6 +7,7 @@
#include "PresetCacheFormat.hpp"
#include "PrintConfig.hpp"
#include "FilamentMixer.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
#include "Utils.hpp"
@@ -79,7 +80,17 @@ static std::vector<std::string> s_project_options {
// whether dynamic per-nozzle filament mapping is active. Persisted with the project and
// restored from a saved 3mf; reset to false on load and set true only by live device sync.
"has_filament_switcher",
"enable_filament_dynamic_map"
"enable_filament_dynamic_map",
// Mixed-color filament slots. Project-level parallel arrays indexed like filament_colour:
// which slots are virtual mixes, their component filaments, blend ratios and the optional
// Z-gradient description. Kept with the project so a saved 3mf round-trips the mix setup.
"filament_is_mixed",
"filament_mixed_components",
"filament_mixed_sublayer_ratios",
"filament_mixed_gradient",
"filament_mixed_gradient_range",
"filament_mixed_gradient_curve",
"filament_mixed_gradient_per_part"
};
//Orca: add custom as default
@@ -2712,6 +2723,79 @@ void PresetBundle::load_installed_sla_materials(AppConfig &config)
preset.set_visible_from_appconfig(config);
}
// Mixed-color filament metadata is project state saved in the 3mf, also mirrored into the app
// config so the last session's mixes are back before any project is opened. It is kept in the
// per-printer snapshot next to the filament list it indexes (filament_%02u/filament_colors),
// because that list is rebuilt on every printer selection and the component ids are 1-based
// indices into exactly that list. Missing keys clear the arrays, so one printer never inherits
// another's mixes; fallback_to_global also reads the shared "presets" keys an older config
// layout used, which export_selections drops on the next save.
static void load_mixed_filament_settings(DynamicPrintConfig &project_config, AppConfig &config,
const std::string &printer_name, size_t n_filaments,
bool fallback_to_global)
{
auto raw_value = [&](const char *key, bool &found) -> std::string {
if (config.has_printer_setting(printer_name, key)) {
found = true;
return config.get_printer_setting(printer_name, key);
}
if (fallback_to_global && config.has("presets", key)) {
found = true;
return config.get("presets", key);
}
found = false;
return std::string{};
};
std::vector<std::string> parts;
auto load_bools = [&](const char *key) {
auto &vals = project_config.option<ConfigOptionBools>(key)->values;
vals.clear();
bool found = false;
const std::string s = raw_value(key, found);
if (found && !s.empty()) {
boost::algorithm::split(parts, s, boost::algorithm::is_any_of(","));
for (const auto &p : parts) vals.push_back(p == "1");
}
vals.resize(n_filaments, false);
};
auto load_strings = [&](const char *key) {
auto &vals = project_config.option<ConfigOptionStrings>(key)->values;
vals.clear();
bool found = false;
const std::string s = raw_value(key, found);
if (found && !s.empty()) {
boost::algorithm::split(parts, s, boost::algorithm::is_any_of("|"));
vals = parts;
}
vals.resize(n_filaments, std::string{});
};
load_bools("filament_is_mixed");
load_strings("filament_mixed_components");
load_strings("filament_mixed_sublayer_ratios");
load_bools("filament_mixed_gradient");
load_strings("filament_mixed_gradient_range");
load_bools("filament_mixed_gradient_per_part");
// The gradient curve is the one array whose values contain '|' themselves (it separates the
// control points), so it is stored C-style escaped rather than '|'-joined.
{
auto &vals = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")->values;
vals.clear();
bool found = false;
const std::string s = raw_value("filament_mixed_gradient_curve", found);
if (found && !s.empty()) {
std::vector<std::string> curves;
if (unescape_strings_cstyle(s, curves))
vals = std::move(curves);
}
vals.resize(n_filaments, std::string{});
// Heal legacy corruption: clear any non-empty slot that ended up with < 2 points
// (e.g. a curve split across slots by the old "|" delimiter). Falls back to linear.
Slic3r::sanitize_mixed_gradient_curve_array(vals);
}
}
void PresetBundle::update_selections(AppConfig &config)
{
std::string initial_printer_profile_name = printers.get_selected_preset_name();
@@ -2792,6 +2876,9 @@ void PresetBundle::update_selections(AppConfig &config)
auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast<double, std::string>);
project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end());
}
// No global fallback here: on a printer change the legacy shared keys describe another
// printer's filament list, so absent per-printer keys must clear the mixes, not revive them.
load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), false);
// Update visibility of presets based on their compatibility with the active printer.
// Always try to select a compatible print and filament preset to the current printer preset,
@@ -2942,6 +3029,7 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast<double, std::string>);
project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end());
}
load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), true);
// Update visibility of presets based on their compatibility with the active printer.
// Always try to select a compatible print and filament preset to the current printer preset,
@@ -3076,6 +3164,32 @@ void PresetBundle::export_selections(AppConfig &config)
"|");
config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str);
// Mixed-color filament metadata goes into the per-printer snapshot next to the filament list
// it indexes (see load_mixed_filament_settings). Bools are ','-joined and the component, ratio
// and range strings '|'-joined; the gradient curve is escaped instead, as it contains '|'.
auto join_bools = [](const std::vector<unsigned char> &vals) {
std::string s;
for (size_t i = 0; i < vals.size(); ++i) {
if (i > 0) s += ",";
s += (vals[i] ? "1" : "0");
}
return s;
};
if (auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed"))
config.set_printer_setting(printer_name, "filament_is_mixed", join_bools(opt->values));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"))
config.set_printer_setting(printer_name, "filament_mixed_components", boost::algorithm::join(opt->values, "|"));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
config.set_printer_setting(printer_name, "filament_mixed_sublayer_ratios", boost::algorithm::join(opt->values, "|"));
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
config.set_printer_setting(printer_name, "filament_mixed_gradient", join_bools(opt->values));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
config.set_printer_setting(printer_name, "filament_mixed_gradient_range", boost::algorithm::join(opt->values, "|"));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
config.set_printer_setting(printer_name, "filament_mixed_gradient_curve", escape_strings_cstyle(opt->values));
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
config.set_printer_setting(printer_name, "filament_mixed_gradient_per_part", join_bools(opt->values));
// BBS
//config.set("presets", "sla_print", sla_prints.get_selected_preset_name());
//config.set("presets", "sla_material", sla_materials.get_selected_preset_name());
@@ -3111,6 +3225,23 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> ne
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
// Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink
// with the filament count exactly like filament_colour above.
if (auto* opt = project_config.option<ConfigOptionBools>("filament_is_mixed"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
opt->values.resize(n, false);
// BBS set new filament color to new_color
if (old_filament_count < n) {
if (!new_colors.empty()) {
@@ -3151,6 +3282,23 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
// Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink
// with the filament count exactly like filament_colour above.
if (auto* opt = project_config.option<ConfigOptionBools>("filament_is_mixed"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
opt->values.resize(n, false);
//BBS set new filament color to new_color
if (old_filament_count < n) {
if (!new_color.empty()) {
@@ -3223,9 +3371,59 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id)
erase_or_resize(filament_color_type->values);
erase_or_resize(ams_multi_color_filment);
// Mixed-color metadata. Component IDs reference other slots by 1-based index, so a deleted
// *physical* filament must be remapped out of every mix before the arrays themselves shrink.
// Deleting a mixed slot needs no remap (nothing references a mixed slot as a component).
{
auto *is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto *comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_opt) {
bool del_is_physical = (to_del_flament_id >= is_mixed_opt->values.size()
|| !is_mixed_opt->values[to_del_flament_id]);
if (del_is_physical)
remap_mixed_components_on_delete(is_mixed_opt->values, comp_opt->values,
to_del_flament_id + 1);
}
if (is_mixed_opt)
erase_or_resize(is_mixed_opt->values);
if (comp_opt)
erase_or_resize(comp_opt->values);
}
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
erase_or_resize(opt->values);
update_multi_material_filament_presets(to_del_flament_id);
}
bool PresetBundle::is_mixed_filament(size_t idx) const
{
auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
return opt && idx < opt->values.size() && opt->values[idx];
}
size_t PresetBundle::num_mixed_filaments() const
{
auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true));
}
std::vector<size_t> PresetBundle::physical_filament_config_indices() const
{
std::vector<size_t> indices;
for (size_t i = 0; i < filament_presets.size(); ++i)
if (!is_mixed_filament(i))
indices.push_back(i);
return indices;
}
void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
{
@@ -3431,6 +3629,63 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
// Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical
// filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in
// would let AMS mapping overwrite it and would break the physical-first slot ordering the
// rest of the feature relies on. The slots are re-appended verbatim after the sync.
struct MixedSlotSnapshot {
std::string preset;
std::string color;
std::string color_type;
std::string mixed_components;
std::string mixed_sublayer_ratios;
bool mixed_gradient = false;
std::string mixed_gradient_range;
std::string mixed_gradient_curve;
bool mixed_gradient_per_part = false;
};
std::vector<MixedSlotSnapshot> mixed_snapshots;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* mixed_comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
auto* mixed_ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
auto* mixed_gradient_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
auto* mixed_grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range");
auto* mixed_grad_curve_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
auto* mixed_per_part_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part");
if (is_mixed_opt) {
for (size_t i = 0; i < is_mixed_opt->values.size() && i < this->filament_presets.size(); ++i) {
if (!is_mixed_opt->values[i])
continue;
MixedSlotSnapshot snap;
snap.preset = this->filament_presets[i];
snap.color = (i < filament_color->values.size()) ? filament_color->values[i] : "";
snap.color_type = (i < filament_color_type->values.size()) ? filament_color_type->values[i] : "";
if (mixed_comp_opt && i < mixed_comp_opt->values.size()) snap.mixed_components = mixed_comp_opt->values[i];
if (mixed_ratios_opt && i < mixed_ratios_opt->values.size()) snap.mixed_sublayer_ratios = mixed_ratios_opt->values[i];
if (mixed_gradient_opt && i < mixed_gradient_opt->values.size()) snap.mixed_gradient = mixed_gradient_opt->values[i];
if (mixed_grad_range_opt && i < mixed_grad_range_opt->values.size()) snap.mixed_gradient_range = mixed_grad_range_opt->values[i];
if (mixed_grad_curve_opt && i < mixed_grad_curve_opt->values.size()) snap.mixed_gradient_curve = mixed_grad_curve_opt->values[i];
if (mixed_per_part_opt && i < mixed_per_part_opt->values.size()) snap.mixed_gradient_per_part = mixed_per_part_opt->values[i];
mixed_snapshots.push_back(snap);
}
if (!mixed_snapshots.empty()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": stripping " << mixed_snapshots.size() << " mixed filament slot(s) before AMS sync";
size_t phys_count = this->filament_presets.size() - mixed_snapshots.size();
this->filament_presets.resize(phys_count);
filament_color->values.resize(phys_count);
filament_color_type->values.resize(phys_count);
filament_map->values.resize(phys_count, 1);
is_mixed_opt->values.resize(phys_count);
if (mixed_comp_opt) mixed_comp_opt->values.resize(phys_count);
if (mixed_ratios_opt) mixed_ratios_opt->values.resize(phys_count);
if (mixed_gradient_opt) mixed_gradient_opt->values.resize(phys_count);
if (mixed_grad_range_opt) mixed_grad_range_opt->values.resize(phys_count);
if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.resize(phys_count);
if (mixed_per_part_opt) mixed_per_part_opt->values.resize(phys_count);
}
}
if (color_only) {
auto get_map_index = [&ams_infos](const std::vector<AMSMapInfo> &infos, const AMSMapInfo &temp) {
for (int i = 0; i < infos.size(); i++) {
@@ -3594,7 +3849,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
return -1;
};
for (size_t i = 0; i < need_append_colors.size(); i++){
if (exist_filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)){
if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){
break;
}
auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color);
@@ -3686,6 +3941,34 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
if (support_interface_filament_opt->value > filament_color_type->values.size())
support_interface_filament_opt->value = 0;
}
// Re-append mixed filament slots that were stripped before AMS sync
if (!mixed_snapshots.empty()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": re-appending " << mixed_snapshots.size() << " mixed filament slot(s) after AMS sync";
size_t new_phys_count = this->filament_presets.size();
if (is_mixed_opt) is_mixed_opt->values.resize(new_phys_count, (unsigned char)false);
if (mixed_comp_opt) mixed_comp_opt->values.resize(new_phys_count);
if (mixed_ratios_opt) mixed_ratios_opt->values.resize(new_phys_count);
if (mixed_gradient_opt) mixed_gradient_opt->values.resize(new_phys_count, (unsigned char)false);
if (mixed_grad_range_opt) mixed_grad_range_opt->values.resize(new_phys_count);
if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.resize(new_phys_count);
if (mixed_per_part_opt) mixed_per_part_opt->values.resize(new_phys_count, (unsigned char)false);
for (auto& snap : mixed_snapshots) {
this->filament_presets.push_back(snap.preset);
filament_color->values.push_back(snap.color);
filament_color_type->values.push_back(snap.color_type);
ams_multi_color_filment.push_back({snap.color});
filament_map->values.push_back(1);
if (is_mixed_opt) is_mixed_opt->values.push_back((unsigned char)true);
if (mixed_comp_opt) mixed_comp_opt->values.push_back(snap.mixed_components);
if (mixed_ratios_opt) mixed_ratios_opt->values.push_back(snap.mixed_sublayer_ratios);
if (mixed_gradient_opt) mixed_gradient_opt->values.push_back((unsigned char)snap.mixed_gradient);
if (mixed_grad_range_opt) mixed_grad_range_opt->values.push_back(snap.mixed_gradient_range);
if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.push_back(snap.mixed_gradient_curve);
if (mixed_per_part_opt) mixed_per_part_opt->values.push_back((unsigned char)snap.mixed_gradient_per_part);
}
}
// Update ams_multi_color_filment
update_filament_multi_color();
update_multi_material_filament_presets();
+6
View File
@@ -497,6 +497,12 @@ public:
// Read out the number of extruders from an active printer preset,
// update size and content of filament_presets.
void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1));
// Mixed-color filament slots: virtual slots realized from 2-3 physical filaments.
bool is_mixed_filament(size_t idx) const;
std::vector<size_t> physical_filament_config_indices() const;
// How many slots are mixed. They sit at the tail of the filament list and have no nozzle of
// their own, so any resize driven by the printer's extruder count has to add this on top.
size_t num_mixed_filaments() const;
void on_extruders_count_changed(int extruder_count);
+114 -30
View File
@@ -6,6 +6,7 @@
#include "Brim.hpp"
#include "ClipperUtils.hpp"
#include "Extruder.hpp"
#include "FilamentMixer.hpp"
#include "Flow.hpp"
#include "Geometry/ConvexHull.hpp"
#include "I18N.hpp"
@@ -566,7 +567,7 @@ std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const
// If a wipe tower filament is explicitly set, ensure it participates in tool ordering.
if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) {
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament < int(config().nozzle_diameter.size()));
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size()));
extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based
}
@@ -1360,6 +1361,19 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
}
// Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on;
// without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and
// the gradient is dropped silently. extruders() already covers painting, height ranges,
// per-feature filament ids and supports, and still lists mixed slots under their own id here.
if (!m_config.enable_mixed_color_sublayer.value) {
const auto &is_mixed = m_config.filament_is_mixed.values;
const auto &gradient = m_config.filament_mixed_gradient.values;
if (std::any_of(extruders.begin(), extruders.end(), [&](unsigned int e) {
return e < is_mixed.size() && is_mixed[e] && e < gradient.size() && gradient[e]; }))
warn(L("A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed."),
"enable_mixed_color_sublayer");
}
if (nozzles < 2 && extruders.size() > 1) {
auto ret = check_multi_filament_valid(*this);
if (!ret.string.empty())
@@ -1421,6 +1435,13 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
// #4043
if (total_copies_count > 1 && m_config.print_sequence != PrintSequence::ByObject)
return {L("Please select \"By object\" print sequence to print multiple objects in spiral vase mode."), nullptr, "spiral_mode"};
// A mixed (virtual) filament always resolves to multiple physical components, which
// spiral vase cannot print.
const auto &is_mixed = m_config.filament_is_mixed.values;
for (const PrintObject *object : m_objects)
for (unsigned int ext : object->object_extruders())
if (ext < is_mixed.size() && is_mixed[ext])
return {L("Spiral (vase) mode does not work when an object contains more than one material."), nullptr, "spiral_mode"};
assert(m_objects.size() == 1);
const auto all_regions = m_objects.front()->all_regions();
if (all_regions.size() > 1) {
@@ -1497,6 +1518,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
if (this->has_wipe_tower() && ! m_objects.empty()) {
// Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after
// resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here
// would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides
// mixed slots from the option; this guards loaded projects and the CLI.
if (m_config.wipe_tower_filament > 0) {
const auto &is_mixed = m_config.filament_is_mixed.values;
const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1);
if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx])
return { L("The wipe tower filament cannot be a mixed filament."), nullptr, "wipe_tower_filament" };
}
// Make sure all extruders use same diameter filament and have the same nozzle diameter
// EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments
double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front());
@@ -2618,18 +2650,31 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::vector<const PrintInstance*>::const_iterator print_object_instance_sequential_active;
std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> layers_to_print = GCode::collect_layers_to_print(*this);
std::vector<unsigned int> printExtruders;
// Per-object first-layer mixed-slot resolutions for the by-object remap below
// (BBS reads them from m_sequential_print_data->object_tool_ordering_map).
std::map<ObjectID, std::map<unsigned int, unsigned int>> seq_mixed_resolution;
// Cleared on every process so a print-sequence or selector-mode change can never leave
// stale object pointers behind; repopulated below only by the sequential selector path.
m_sequential_dynamic_orderings.clear();
if (this->config().print_sequence == PrintSequence::ByObject) {
// Order object instances for sequential print.
print_object_instances_ordering = sort_object_instances_by_model_order(*this);
// A mixed slot is virtual; only its components reach a nozzle. These per-object orderings
// are unsorted (no resolve_mixed_filaments), so expand the slots here for the grouping, the
// unprintable sets and the slice-used lists. Because the expansion happens here rather than
// on the sorted orderings, the first-layer used set lists every component of a mixed slot,
// not just the one layer 0 resolves to. No-op without mixed filaments.
const auto &is_mixed = m_config.filament_is_mixed.values;
const auto &comp_strs = m_config.filament_mixed_components.values;
const bool has_mixed = has_any_mixed_filament(is_mixed);
std::vector<unsigned int> first_layer_used_filaments;
std::vector<std::vector<unsigned int>> all_filaments;
for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) {
auto& layer_filament = tool_ordering.layer_tools()[idx].extruders;
auto layer_filament = tool_ordering.layer_tools()[idx].extruders;
if (has_mixed)
layer_filament = expand_mixed_filaments(layer_filament, is_mixed, comp_strs);
all_filaments.emplace_back(layer_filament);
if (idx == 0)
first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end());
@@ -2641,6 +2686,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments);
auto geometric_unprintables = this->get_geometric_unprintable_filaments();
if (has_mixed)
expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs);
auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments);
// Selector (per-layer regroup) prints skip the static grouping: their print-wide result
// is stitched from the per-object plans after the ordering loop below.
@@ -2692,6 +2739,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::vector<std::vector<int>> nozzle_map_per_layer;
std::vector<std::vector<unsigned int>> stitched_layer_filaments;
print_object_instance_sequential_active = print_object_instances_ordering.begin();
std::vector<unsigned int> used_mixed_filaments;
for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object;
if (dynamic_reorder) {
@@ -2720,11 +2768,18 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
} else {
tool_ordering = ToolOrdering(*print_object, initial_extruder_id);
tool_ordering.sort_and_build_data(*print_object, initial_extruder_id);
if (!tool_ordering.layer_tools().empty())
seq_mixed_resolution[print_object->id()] = tool_ordering.layer_tools().front().mixed_filament_resolution;
}
// Only sorted orderings have run resolve_mixed_filaments, so only they know which
// mixed slots actually print.
append(used_mixed_filaments, tool_ordering.used_mixed_filaments());
if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast<unsigned int>(-1)) {
append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders);
}
}
sort_remove_duplicates(used_mixed_filaments);
this->set_slice_used_mixed_filaments(used_mixed_filaments);
if (dynamic_reorder && m_objects.size() > 1) {
// Stitch the per-object plans into one print-wide selector result. A single-object
// sequential print publishes (and writes back) from its own ordering instead: the
@@ -2745,6 +2800,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
first_layer_used_filaments = tool_ordering.layer_tools().front().extruders;
this->set_slice_used_filaments(first_layer_used_filaments, tool_ordering.all_extruders());
this->set_slice_used_mixed_filaments(tool_ordering.used_mixed_filaments());
has_wipe_tower = this->has_wipe_tower() && tool_ordering.has_wipe_tower();
initial_extruder_id = tool_ordering.first_extruder();
print_object_instances_ordering = chain_print_object_instances(*this);
@@ -2752,6 +2808,28 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
auto objectExtruderMap = getObjectExtruderMap(*this);
// Resolve mixed filament virtual slots to physical components so brim
// extruder matching works correctly (mixed slot IDs are not present
// in printExtruders after ToolOrdering::resolve_mixed_filaments).
{
const LayerTools *first_lt = nullptr;
if (m_config.print_sequence != PrintSequence::ByObject && !tool_ordering.layer_tools().empty())
first_lt = &tool_ordering.layer_tools().front();
for (auto &[obj_id, ext_1based] : objectExtruderMap) {
if (ext_1based == 0)
continue;
const std::map<unsigned int, unsigned int> *resolution = nullptr;
if (first_lt)
resolution = &first_lt->mixed_filament_resolution;
else if (auto obj_it = seq_mixed_resolution.find(obj_id); obj_it != seq_mixed_resolution.end())
resolution = &obj_it->second;
if (resolution) {
auto it = resolution->find(ext_1based - 1);
if (it != resolution->end())
ext_1based = it->second + 1;
}
}
}
std::vector<std::pair<ObjectID, unsigned int>> objPrintVec;
for (const PrintInstance* instance : print_object_instances_ordering) {
const ObjectID& print_object_ID = instance->print_object->id();
@@ -3820,6 +3898,14 @@ bool Print::is_dynamic_group_reorder() const
const bool enabled = opt && opt->value;
if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1)
return false;
// Dynamic regrouping and mixed-color slots are incompatible: a mixed slot is resolved to
// different physical components per layer, so a group assignment made up-front would be wrong.
const auto &is_mixed = m_config.filament_is_mixed.values;
for (unsigned int filament_id : extruders()) {
if (filament_id < is_mixed.size() && is_mixed[filament_id])
return false;
}
return true;
}
@@ -4043,38 +4129,36 @@ void Print::_make_wipe_tower()
return;
// Check whether there are any layers in m_tool_ordering, which are marked with has_wipe_tower,
// they print neither object, nor support. These layers are above the raft and below the object, and they
// shall be added to the support layers to be printed.
// see https://github.com/prusa3d/PrusaSlicer/issues/607
// they print neither object, nor support. Each such layer needs a virtual support layer
// counterpart in m_objects.front() so that GCode::collect_layers_to_print picks it up and the
// wipe tower G-code is actually emitted for that z. Such layers appear in two scenarios:
// - above the raft, between raft top and the first real object layer
// (see https://github.com/prusa3d/PrusaSlicer/issues/607);
// - between two real wipe-tower layers, when one object is fully floating above another and
// the support_top_z_distance / support_bottom_z_distance gap leaves interior z values with
// neither object nor support (continuity fill in ToolOrdering::fill_wipe_tower_partitions).
// The previous implementation only handled the first contiguous run starting at the first
// virtual layer, which made the second scenario silently produce empty wipe-tower layers.
{
size_t idx_begin = size_t(-1);
size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
// Find the first wipe tower layer, which does not have a counterpart in an object or a support layer.
auto &support_layers = m_objects.front()->support_layers();
auto it_layer = support_layers.begin();
const size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
for (size_t i = 0; i < idx_end; ++ i) {
const LayerTools &lt = m_wipe_tower_data.tool_ordering.layer_tools()[i];
if (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support) {
idx_begin = i;
break;
}
}
if (idx_begin != size_t(-1)) {
// Find the position in m_objects.first()->support_layers to insert these new support layers.
double wipe_tower_new_layer_print_z_first = m_wipe_tower_data.tool_ordering.layer_tools()[idx_begin].print_z;
auto it_layer = m_objects.front()->support_layers().begin();
auto it_end = m_objects.front()->support_layers().end();
for (; it_layer != it_end && (*it_layer)->print_z - EPSILON < wipe_tower_new_layer_print_z_first; ++ it_layer);
// Find the stopper of the sequence of wipe tower layers, which do not have a counterpart in an object or a support layer.
for (size_t i = idx_begin; i < idx_end; ++ i) {
LayerTools &lt = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
break;
lt.has_support = true;
// Insert the new support layer.
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
LayerTools &lt = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
continue;
while (it_layer != support_layers.end() && (*it_layer)->print_z + EPSILON < lt.print_z)
++ it_layer;
if (it_layer != support_layers.end() && std::abs((*it_layer)->print_z - lt.print_z) < EPSILON) {
lt.has_support = true;
++ it_layer;
continue;
}
lt.has_support = true;
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
++ it_layer;
}
}
this->throw_if_canceled();
+23 -4
View File
@@ -117,9 +117,9 @@ class PrintRegion
public:
PrintRegion() = default;
PrintRegion(const PrintRegionConfig &config);
PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {}
PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {}
PrintRegion(PrintRegionConfig &&config);
PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {}
PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {}
~PrintRegion() = default;
// Methods NOT modifying the PrintRegion's state:
@@ -129,6 +129,10 @@ public:
// Identifier of this PrintRegion in the list of Print::m_print_regions.
int print_region_id() const throw() { return m_print_region_id; }
int print_object_region_id() const throw() { return m_print_object_region_id; }
// Volume identity used to differentiate same-config regions when per-part gradient is enabled.
// Default-constructed (invalid) means this region is not tied to a specific volume — preserves
// existing behavior for all paths not using per_part_gradient.
ObjectID gradient_volume_id() const throw() { return m_gradient_volume_id; }
// 1-based extruder identifier for this region and role.
unsigned int extruder(FlowRole role) const;
Flow flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer = false) const;
@@ -158,6 +162,10 @@ private:
int m_print_region_id { -1 };
int m_print_object_region_id { -1 };
int m_ref_cnt { 0 };
// Per-part gradient: when non-invalid, this region belongs exclusively to one ModelVolume,
// letting same-color volumes within a combined ModelObject be tracked separately for gradient
// emission. Default invalid -> region keying behaves exactly as before.
ObjectID m_gradient_volume_id;
};
inline bool operator==(const PrintRegion &lhs, const PrintRegion &rhs) { return lhs.config_hash() == rhs.config_hash() && lhs.config() == rhs.config(); }
@@ -306,6 +314,11 @@ public:
Transform3d trafo_bboxes;
std::vector<ObjectID> cached_volume_ids;
// Per-part gradient: the slot_per_part_enabled bit vector that produced these regions.
// Print::apply compares it against the current one to detect a change that PrintRegionConfig
// alone would not reveal, and regenerates the regions when it differs.
std::vector<bool> last_slot_per_part_enabled;
void ref_cnt_inc() { ++ m_ref_cnt; }
void ref_cnt_dec() { if (-- m_ref_cnt == 0) delete this; }
void clear() {
@@ -930,8 +943,8 @@ public:
// If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r).
std::string export_gcode(const std::string& path_template, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
//return 0 means successful
int export_cached_data(const std::string& dir_path, bool with_space=false);
int load_cached_data(const std::string& directory);
int export_cached_data(const std::string& dir_path, bool with_space=false) override;
int load_cached_data(const std::string& directory) override;
// methods for handling state
bool is_step_done(PrintStep step) const { return Inherited::is_step_done(step); }
@@ -1075,6 +1088,10 @@ public:
m_slice_used_filaments = used_filaments;
}
std::vector<unsigned int> get_slice_used_filaments(bool first_layer) const { return first_layer ? m_slice_used_filaments_first_layer : m_slice_used_filaments;}
void set_slice_used_mixed_filaments(const std::vector<unsigned int> &used_mixed_filaments) {
m_slice_used_mixed_filaments = used_mixed_filaments;
}
const std::vector<unsigned int>& get_slice_used_mixed_filaments() const { return m_slice_used_mixed_filaments; }
/**
* @brief Determines the unprintable filaments for each extruder based on its physical attributes
@@ -1348,6 +1365,8 @@ private:
std::vector<unsigned int> m_slice_used_filaments;
std::vector<unsigned int> m_slice_used_filaments_first_layer;
// 0-based mixed (virtual) filament slots actually used on this plate.
std::vector<unsigned int> m_slice_used_mixed_filaments;
//BBS: plate's origin
Vec3d m_origin {0, 0, 0};
+124 -10
View File
@@ -2,6 +2,7 @@
#include "IMEXHelpers.hpp"
#include "Model.hpp"
#include "Print.hpp"
#include "FilamentMixer.hpp"
#include <boost/log/trivial.hpp>
#include <cfloat>
@@ -887,7 +888,12 @@ bool verify_update_print_object_regions(
size_t hash = regions[i]->config_hash();
size_t j = i;
for (++ j; j < regions.size() && regions[j]->config_hash() == hash; ++ j)
if (regions[i]->config() == regions[j]->config()) {
// Same config but different gradient_volume_id is intentional (per-part gradient
// splitting) and must NOT be flagged as a merge. When per-part is off all regions
// carry an invalid (default) gradient_volume_id, so the AND condition is always
// true and behavior matches the legacy check.
if (regions[i]->config() == regions[j]->config()
&& regions[i]->gradient_volume_id() == regions[j]->gradient_volume_id()) {
// Regions were merged. We need to reslice.
return false;
}
@@ -979,7 +985,10 @@ static PrintObjectRegions* generate_print_object_regions(
const float xy_contour_compensation,
const std::vector<unsigned int> &painting_extruders,
std::vector<int> &variant_index,
const bool has_painted_fuzzy_skin)
const bool has_painted_fuzzy_skin,
// Per-part gradient: slot_per_part_enabled[s-1] is true when mixed slot s has
// filament_mixed_gradient_per_part on. Empty / all-false preserves legacy behavior.
const std::vector<bool> &slot_per_part_enabled = {})
{
// Reuse the old object or generate a new one.
auto out = print_object_regions_old ? std::unique_ptr<PrintObjectRegions>(print_object_regions_old) : std::make_unique<PrintObjectRegions>();
@@ -1014,19 +1023,71 @@ static PrintObjectRegions* generate_print_object_regions(
update_volume_bboxes(layer_ranges_regions, out->cached_volume_ids, model_volumes, out->trafo_bboxes, is_mm_painted ? 0.f : std::max(0.f, xy_contour_compensation));
std::vector<PrintRegion*> region_set;
auto get_create_region = [&region_set, &all_regions](PrintRegionConfig &&config) -> PrintRegion* {
// Look up or create a PrintRegion. The optional volume_tag, when valid (non-zero ObjectID),
// keys the region to one ModelVolume so two volumes with identical settings still get
// separate regions — needed so each part can run its own gradient. A default (invalid)
// tag reproduces the previous lookup exactly.
auto get_create_region = [&region_set, &all_regions](PrintRegionConfig &&config, ObjectID volume_tag = ObjectID()) -> PrintRegion* {
size_t hash = config.hash();
auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash](const PrintRegion* l) {
return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config); });
if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config)
auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash, volume_tag](const PrintRegion* l) {
return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config)
|| (l->config_hash() == hash && l->config() == config && l->gradient_volume_id() < volume_tag); });
if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config
&& (*it)->gradient_volume_id() == volume_tag)
return *it;
// Insert into a sorted array, it has O(n) complexity, but the calling algorithm has an O(n^2*log(n)) complexity anyways.
all_regions.emplace_back(std::make_unique<PrintRegion>(std::move(config), hash, int(all_regions.size())));
all_regions.emplace_back(std::make_unique<PrintRegion>(std::move(config), hash, int(all_regions.size()), volume_tag));
PrintRegion *region = all_regions.back().get();
region_set.emplace(it, region);
return region;
};
// Per-part gradient: count how many model-part volumes in this object use each
// per-part-enabled gradient slot. Only slots with at least 2 users get their volumes
// tagged — a single-user slot gains nothing from per-volume splitting and would only
// inflate the region count. Empty slot_per_part_enabled leaves this empty, so
// compute_volume_tag below always returns an invalid tag and nothing changes.
std::vector<int> per_part_volume_users;
if (!slot_per_part_enabled.empty()) {
per_part_volume_users.assign(slot_per_part_enabled.size(), 0);
for (const ModelVolume *mv : model_volumes) {
if (! mv->is_model_part())
continue;
const DynamicPrintConfig *range_cfg = layer_ranges_regions.empty() ? nullptr : layer_ranges_regions.front().config;
PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, range_cfg, *mv, num_extruders, variant_index);
for (unsigned int s_1based : { (unsigned int)vol_cfg.outer_wall_filament_id.value,
(unsigned int)vol_cfg.inner_wall_filament_id.value,
(unsigned int)vol_cfg.sparse_infill_filament_id.value,
(unsigned int)vol_cfg.internal_solid_filament_id.value,
(unsigned int)vol_cfg.top_surface_filament_id.value,
(unsigned int)vol_cfg.bottom_surface_filament_id.value }) {
if (s_1based >= 1
&& size_t(s_1based - 1) < slot_per_part_enabled.size()
&& slot_per_part_enabled[s_1based - 1])
++per_part_volume_users[s_1based - 1];
}
}
}
auto compute_volume_tag = [&](const PrintRegionConfig &cfg, const ModelVolume &mv) -> ObjectID {
if (per_part_volume_users.empty())
return ObjectID();
auto qualifies = [&](unsigned int s_1based) {
return s_1based >= 1
&& size_t(s_1based - 1) < slot_per_part_enabled.size()
&& slot_per_part_enabled[s_1based - 1]
&& per_part_volume_users[s_1based - 1] >= 2;
};
if (qualifies((unsigned int)cfg.outer_wall_filament_id.value)
|| qualifies((unsigned int)cfg.inner_wall_filament_id.value)
|| qualifies((unsigned int)cfg.sparse_infill_filament_id.value)
|| qualifies((unsigned int)cfg.internal_solid_filament_id.value)
|| qualifies((unsigned int)cfg.top_surface_filament_id.value)
|| qualifies((unsigned int)cfg.bottom_surface_filament_id.value)) {
return mv.id();
}
return ObjectID();
};
// Chain the regions in the order they are stored in the volumes list.
for (int volume_id = 0; volume_id < int(model_volumes.size()); ++ volume_id) {
const ModelVolume &volume = *model_volumes[volume_id];
@@ -1035,9 +1096,11 @@ static PrintObjectRegions* generate_print_object_regions(
if (const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, volume); bbox) {
if (volume.is_model_part()) {
// Add a model volume, assign an existing region or generate a new one.
PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index);
ObjectID volume_tag = compute_volume_tag(vol_cfg, volume);
layer_range.volume_regions.push_back({
&volume, -1,
get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index)),
get_create_region(std::move(vol_cfg), volume_tag),
bbox
});
} else if (volume.is_negative_volume()) {
@@ -1122,6 +1185,12 @@ static PrintObjectRegions* generate_print_object_regions(
}
}
// Save the slot_per_part_enabled bit vector that produced these regions, so the guard in
// Print::apply can detect changes on the next call even when PrintRegionConfig did not
// change. Always written — including an empty vector — so the snapshot always reflects
// the exact input used to generate the current regions.
out->last_slot_per_part_enabled = slot_per_part_enabled;
return out.release();
}
@@ -1142,6 +1211,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
std::vector <unsigned int> used_filaments = this->extruders(true);
std::unordered_set <unsigned int> used_filament_set(used_filaments.begin(), used_filaments.end());
// A mixed slot is virtual: the filaments actually consumed are its components, so add them
// to the used set or they would be treated as unused and stripped from the config.
{
auto* is_mixed_opt = new_full_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = new_full_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
auto expanded = expand_mixed_filaments(used_filaments, is_mixed_opt->values, comp_strs_opt->values);
used_filament_set.insert(expanded.begin(), expanded.end());
}
}
//new_full_config.normalize_fdm(used_filaments);
new_full_config.normalize_fdm_1();
t_config_option_keys changed_keys = new_full_config.normalize_fdm_2(objects().size(), used_filaments.size());
@@ -1820,6 +1900,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
update_filament_self_index_cache();
}
// Per-part gradient: compute the per-slot enable bit vector once for this Print::apply pass.
// Used by generate_print_object_regions to decide which volumes deserve their own PrintRegion.
std::vector<bool> slot_per_part_enabled;
{
const auto &is_mixed_vec = m_config.filament_is_mixed.values;
const auto &grad_vec = m_config.filament_mixed_gradient.values;
const auto &per_part_vec = m_config.filament_mixed_gradient_per_part.values;
const auto &components_vec = m_config.filament_mixed_components.values;
slot_per_part_enabled.assign(is_mixed_vec.size(), false);
for (size_t i = 0; i < is_mixed_vec.size(); ++i) {
if (! is_mixed_vec[i])
continue;
std::vector<unsigned int> comps = parse_mixed_components(i < components_vec.size() ? components_vec[i] : "");
if (comps.size() != 2)
continue;
if (i >= grad_vec.size() || ! grad_vec[i])
continue;
if (i >= per_part_vec.size() || ! per_part_vec[i])
continue;
slot_per_part_enabled[i] = true;
}
}
// All regions now have distinct settings.
// Check whether applying the new region config defaults we would get different regions,
// update regions or create regions from scratch.
@@ -1846,7 +1949,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (const ModelVolume *volume : volumes) {
const std::vector<bool> &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states;
assert(volume_used_facet_states.size() == used_facet_states.size());
// Paint data saved before the painted state range was extended deserializes a
// shorter used_states vector, so merge over the common prefix.
for (size_t state_idx = 0; state_idx < std::min(volume_used_facet_states.size(), used_facet_states.size()); ++state_idx)
used_facet_states[state_idx] |= volume_used_facet_states[state_idx];
}
@@ -1880,6 +1984,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys));
},
print_variant_index)) {
// Per-part gradient: PrintRegionConfig alone cannot reveal a change in which slots
// have per-part enabled, so compare against the snapshot taken when these regions
// were generated and regenerate on any difference (slot toggled, per-part moved
// between slots, eligibility changed via components / gradient / is_mixed).
if (print_object_regions->last_slot_per_part_enabled != slot_per_part_enabled) {
invalidate();
model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid;
print_regions_reshuffled = true;
}
// Regions are valid, just keep them.
} else {
// Regions were reshuffled.
@@ -1902,7 +2015,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value),
painting_extruders,
print_variant_index,
print_object.is_fuzzy_skin_painted());
print_object.is_fuzzy_skin_painted(),
slot_per_part_enabled);
}
for (auto it = it_print_object; it != it_print_object_end; ++it)
if ((*it)->m_shared_regions) {
+91 -1
View File
@@ -2,6 +2,7 @@
#include "PrintConfigConstants.hpp"
#include "ClipperUtils.hpp"
#include "Config.hpp"
#include "FilamentMixer.hpp"
#include "MaterialType.hpp"
#include "I18N.hpp"
#include "format.hpp"
@@ -3282,6 +3283,62 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBools { false });
// Mixed-color filament. A slot flagged here is virtual: it is not loaded into any
// physical extruder, but resolved at slicing time into the physical filaments listed
// in filament_mixed_components, blended either by splitting each layer into
// sub-layers or by alternating whole layers (see enable_mixed_color_sublayer).
def = this->add("filament_is_mixed", coBools);
def->label = L("Is mixed filament");
def->tooltip = L("Whether this filament slot is a mixed filament composed of multiple physical filaments");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBools{false});
def = this->add("filament_mixed_components", coStrings);
def->label = L("Mixed filament components");
def->tooltip = L("Comma-separated 1-based indices of component filaments, e.g. \"1,3\"");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_sublayer_ratios", coStrings);
def->label = L("Mixed filament sublayer ratios");
def->tooltip = L("Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_gradient", coBools);
def->label = L("Mixed filament gradient");
def->tooltip = L("Enable Z-direction gradient mode for mixed filament sub-layers. "
"When enabled, the sub-layer ratios vary linearly across layers.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBools{false});
def = this->add("filament_mixed_gradient_range", coStrings);
def->label = L("Mixed filament gradient range");
def->tooltip = L("Start and end ratios for the first component in gradient mode. "
"Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_gradient_curve", coStrings);
def->label = L("Mixed filament gradient curve");
def->tooltip = L("Optional Photoshop-style custom curve mapping Z progress to the first "
"component ratio. Encoded as pipe-separated control points, "
"either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override "
"is needed (empty token or \"nan\" means use PCHIP default). "
"x in [0,1]; y is clamped to the configured ratio range, "
"e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear "
"gradient_range is used instead.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_gradient_per_part", coBools);
def->label = L("Mixed filament per-part gradient");
def->tooltip = L("When gradient mode is enabled, apply the gradient to each part of an "
"assembly independently rather than treating the whole assembly as one "
"Z range.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBools{false});
// defined in bits
// 0 means cannot support, 1 means support
// 0 bit: can support in left extruder
@@ -7549,6 +7606,14 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloats { 1. });
def = this->add("enable_mixed_color_sublayer", coBool);
def->label = L("Mixed color sublayer");
def->tooltip = L("Enable mixed color sublayer splitting. When enabled, layers containing mixed color "
"filaments will be split into sub-layers to achieve color mixing effects.");
def->category = L("Quality");
def->mode = comSimple;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("enable_prime_tower", coBool);
def->label = L("Enable");
def->tooltip = L("The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects.");
@@ -9752,7 +9817,15 @@ t_config_option_keys DynamicPrintConfig::normalize_fdm_2(int num_objects, int us
ConfigOptionBool *enable_wrapping_opt = this->option<ConfigOptionBool>("enable_wrapping_detection");
bool enable_wrapping = enable_wrapping_opt != nullptr && enable_wrapping_opt->value;
if (!is_smooth_timelapse && !enable_wrapping && (used_filaments == 1 || (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) {
bool has_mixed_filament = false;
{
auto *mixed_opt = this->option<ConfigOptionBools>("filament_is_mixed");
if (mixed_opt)
has_mixed_filament = has_any_mixed_filament(mixed_opt->values);
}
if (!is_smooth_timelapse && !enable_wrapping
&& ( (used_filaments == 1 && !has_mixed_filament)
|| (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) {
if (ept_opt->value) {
ept_opt->value = false;
changed_keys.push_back("enable_prime_tower");
@@ -11900,6 +11973,23 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
}
}
// Mixed-color (混色) parameter validation.
{
const auto &is_mixed = cfg.filament_is_mixed.values;
const auto &comp_strs = cfg.filament_mixed_components.values;
const auto &ratio_strs = cfg.filament_mixed_sublayer_ratios.values;
const auto &gradient_flags = cfg.filament_mixed_gradient.values;
const auto &range_strs = cfg.filament_mixed_gradient_range.values;
const auto &curve_strs = cfg.filament_mixed_gradient_curve.values;
std::map<std::string, std::string> mixed_errors = validate_mixed_filament_params(
is_mixed, comp_strs, ratio_strs, gradient_flags,
range_strs, curve_strs);
for (const auto &kv : mixed_errors)
if (error_message.find(kv.first) == error_message.end())
error_message.emplace(kv.first, kv.second);
}
// The configuration is valid.
return error_message;
}
+9
View File
@@ -1565,6 +1565,14 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, filament_colour))
((ConfigOptionStrings, filament_vendor))
((ConfigOptionBools, filament_is_support))
// Mixed-color filament: a virtual slot realized from 2-3 physical filaments.
((ConfigOptionBools, filament_is_mixed))
((ConfigOptionStrings, filament_mixed_components))
((ConfigOptionStrings, filament_mixed_sublayer_ratios))
((ConfigOptionBools, filament_mixed_gradient))
((ConfigOptionStrings, filament_mixed_gradient_range))
((ConfigOptionStrings, filament_mixed_gradient_curve))
((ConfigOptionBools, filament_mixed_gradient_per_part))
((ConfigOptionInts, filament_printable))
((ConfigOptionInts, filament_extruder_compatibility))
((ConfigOptionFloats, filament_change_length))
@@ -1875,6 +1883,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionInts, nozzle_temperature_range_low))
((ConfigOptionInts, nozzle_temperature_range_high))
((ConfigOptionFloats, wipe_distance))
((ConfigOptionBool, enable_mixed_color_sublayer))
((ConfigOptionBool, enable_prime_tower))
((ConfigOptionBool, prime_tower_enable_framework))
// BBS: change wipe_tower_x and wipe_tower_y data type to floats to add partplate logic
+2
View File
@@ -1,4 +1,6 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
#include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
+726
View File
@@ -0,0 +1,726 @@
#include "TexturePainting.hpp"
#include <algorithm>
#include <cmath>
#include <map>
#include <set>
#include <utility>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <boost/log/trivial.hpp>
#include "TextureToColor/TextureToColor.hpp"
#include "TextureToColor/ColorUtils.hpp"
#include "Model.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
static cv::Mat decode_texture_image(const TextureImage& img) {
if (img.data.empty())
return {};
// Raw encoded image data (PNG/JPEG) from glTF loader: width == -1
if (img.width <= 0 || img.height <= 0) {
std::vector<unsigned char> buf(img.data.begin(), img.data.end());
cv::Mat raw(1, static_cast<int>(buf.size()), CV_8UC1, buf.data());
cv::Mat decoded = cv::imdecode(raw, cv::IMREAD_COLOR);
return decoded;
}
int cv_type = (img.channels == 4) ? CV_8UC4 : CV_8UC3;
std::vector<unsigned char> pixel_buf(img.data.begin(), img.data.end());
cv::Mat src(img.height, img.width, cv_type, pixel_buf.data());
cv::Mat bgr;
if (img.channels == 4)
cv::cvtColor(src, bgr, cv::COLOR_RGBA2BGR);
else if (img.channels == 3)
cv::cvtColor(src, bgr, cv::COLOR_RGB2BGR);
else
return {};
return bgr;
}
static void build_tex2color_mesh(
const TexturedMesh& textured,
tex2color::TriMesh& mesh,
std::vector<std::vector<Vec2f>>& uv_coords)
{
const size_t nv = textured.vertices.size();
const size_t nf = textured.indices.size();
mesh.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i) {
mesh.vertices[i] = Vec3f(
textured.vertices[i][0],
textured.vertices[i][1],
textured.vertices[i][2]);
}
mesh.indices.resize(nf);
for (size_t i = 0; i < nf; ++i) {
mesh.indices[i] = Vec3i32(
textured.indices[i][0],
textured.indices[i][1],
textured.indices[i][2]);
}
uv_coords.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
uv_coords[fi].resize(3);
for (int vi = 0; vi < 3; ++vi) {
if (textured.has_face_uvs()) {
int uv_idx = textured.uv_indices[fi][vi];
if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) {
uv_coords[fi][vi] = Vec2f(
textured.uv_coords[uv_idx][0],
textured.uv_coords[uv_idx][1]);
} else {
uv_coords[fi][vi] = Vec2f(0.f, 0.f);
}
} else {
int vtx_idx = textured.indices[fi][vi];
if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) {
uv_coords[fi][vi] = Vec2f(
textured.uvs[vtx_idx][0],
textured.uvs[vtx_idx][1]);
} else {
uv_coords[fi][vi] = Vec2f(0.f, 0.f);
}
}
}
}
}
static void extract_painted_mesh(
const tex2color::TriMesh& color_mesh,
const std::vector<std::array<std::size_t,3>>& face_colors,
PaintedMesh& painted)
{
const size_t nv = color_mesh.vertices.size();
const size_t nf = color_mesh.indices.size();
painted.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i) {
const auto& v = color_mesh.vertices[i];
painted.vertices[i] = {v.x(), v.y(), v.z()};
}
painted.indices.resize(nf);
for (size_t i = 0; i < nf; ++i) {
const auto& f = color_mesh.indices[i];
painted.indices[i] = {f[0], f[1], f[2]};
}
painted.face_colors = face_colors;
std::set<std::array<std::size_t,3>> unique_colors(face_colors.begin(), face_colors.end());
painted.cluster_colors.assign(unique_colors.begin(), unique_colors.end());
}
// Build a vertically-stacked atlas from multiple textures and remap per-face UVs.
//
// Sub-textures are laid out left-aligned (x=0) at successive y offsets, with
// atlas_w taken as the maximum width across all sub-textures. UVs must therefore
// be remapped on BOTH axes so that faces belonging to a sub-texture narrower
// than atlas_w sample inside that sub-texture's region (left side of the atlas)
// instead of the right-side zero-padding. Materials that carry only a baseColor
// (no map_Kd / glTF baseColorTexture) get their own 1x1 swatch at the bottom of
// the atlas so their faces sample the correct flat colour rather than being
// silently aliased onto textures[0].
static bool build_multi_texture_atlas(
const TexturedMesh& textured,
cv::Mat& out_atlas,
std::vector<std::vector<Vec2f>>& out_uv_coords)
{
std::vector<cv::Mat> decoded;
decoded.reserve(textured.textures.size());
for (const auto& ti : textured.textures)
decoded.push_back(decode_texture_image(ti));
const bool has_mapping = !textured.material_texture_map.empty();
const size_t nf = textured.indices.size();
auto resolve_tex_idx = [&](int mat_idx) -> int {
if (!has_mapping || mat_idx < 0
|| static_cast<size_t>(mat_idx) >= textured.material_texture_map.size())
return -1;
const int ti = textured.material_texture_map[mat_idx];
if (ti < 0 || static_cast<size_t>(ti) >= decoded.size() || decoded[ti].empty())
return -1;
return ti;
};
// Determine atlas width (max width across all textures) and per-texture row offsets.
int atlas_w = 0;
int atlas_h = 0;
std::vector<int> y_offsets(decoded.size(), 0);
int first_usable_tex = -1;
for (size_t i = 0; i < decoded.size(); ++i) {
if (decoded[i].empty()) continue;
if (first_usable_tex < 0) first_usable_tex = static_cast<int>(i);
y_offsets[i] = atlas_h;
atlas_w = std::max(atlas_w, decoded[i].cols);
atlas_h += decoded[i].rows;
}
if (atlas_w == 0 || atlas_h == 0)
return false;
// Collect materials that have a baseColor but no usable texture so we can
// route their faces to a dedicated 1x1 solid swatch instead of aliasing
// them onto textures[0].
std::map<int, int> mat_solid_y; // mat_idx -> y row in atlas
std::map<int, std::array<float,4>> mat_solid_color; // mat_idx -> baseColor (RGBA)
for (size_t fi = 0; fi < nf; ++fi) {
const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
if (mat_idx < 0) continue;
if (resolve_tex_idx(mat_idx) >= 0) continue;
if (static_cast<size_t>(mat_idx) >= textured.material_colors.size()) continue;
if (mat_solid_y.find(mat_idx) != mat_solid_y.end()) continue;
mat_solid_y[mat_idx] = atlas_h++;
mat_solid_color[mat_idx] = textured.material_colors[mat_idx];
}
out_atlas = cv::Mat::zeros(atlas_h, atlas_w, CV_8UC3);
for (size_t i = 0; i < decoded.size(); ++i) {
if (decoded[i].empty()) continue;
cv::Mat roi = out_atlas(cv::Rect(0, y_offsets[i], decoded[i].cols, decoded[i].rows));
decoded[i].copyTo(roi);
}
for (const auto& kv : mat_solid_color) {
const auto& c = kv.second;
// OpenCV stores BGR; baseColor is RGBA in [0,1].
out_atlas.at<cv::Vec3b>(mat_solid_y[kv.first], 0) = cv::Vec3b(
static_cast<uchar>(std::clamp(c[2] * 255.f, 0.f, 255.f)),
static_cast<uchar>(std::clamp(c[1] * 255.f, 0.f, 255.f)),
static_cast<uchar>(std::clamp(c[0] * 255.f, 0.f, 255.f)));
}
out_uv_coords.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
const int tex_idx = resolve_tex_idx(mat_idx);
// Pick the atlas region this face samples from.
int y_off = 0, x_off = 0, th = atlas_h, tw = atlas_w;
bool use_solid = false;
if (tex_idx >= 0) {
y_off = y_offsets[tex_idx];
th = decoded[tex_idx].rows;
tw = decoded[tex_idx].cols;
} else if (mat_idx >= 0 && mat_solid_y.count(mat_idx) > 0) {
y_off = mat_solid_y[mat_idx];
th = 1;
tw = 1;
use_solid = true;
} else if (first_usable_tex >= 0) {
// Last-resort fallback: faces without a material or without any
// baseColor still need somewhere to sample; the first usable
// texture preserves legacy behaviour and, with the per-axis
// remapping below, no longer aliases onto the zero-padded right
// margin even when sub-textures have unequal widths.
y_off = y_offsets[first_usable_tex];
th = decoded[first_usable_tex].rows;
tw = decoded[first_usable_tex].cols;
}
out_uv_coords[fi].resize(3);
for (int vi = 0; vi < 3; ++vi) {
float u = 0.f, v = 0.f;
if (textured.has_face_uvs()) {
int uv_idx = textured.uv_indices[fi][vi];
if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) {
u = textured.uv_coords[uv_idx][0];
v = textured.uv_coords[uv_idx][1];
}
} else {
int vtx_idx = textured.indices[fi][vi];
if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) {
u = textured.uvs[vtx_idx][0];
v = textured.uvs[vtx_idx][1];
}
}
if (use_solid) {
// Aim at the centre of the 1x1 swatch so bilinear sampling
// (in tex2color) cannot drift into neighbouring rows.
const float u_atlas = (x_off + 0.5f) / static_cast<float>(atlas_w);
const float v_atlas = (y_off + 0.5f) / static_cast<float>(atlas_h);
out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas);
} else {
// Wrap to [0,1) on both axes (OBJ tile UVs may step outside
// the unit square), then scale by the sub-texture extents so
// samples land inside its actual region. Without scaling u,
// any sub-texture narrower than atlas_w would have all its
// faces sampled from the right-side zero-padding.
u = u - std::floor(u);
v = v - std::floor(v);
const float u_atlas = (x_off + u * tw) / static_cast<float>(atlas_w);
const float v_atlas = (y_off + v * th) / static_cast<float>(atlas_h);
out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas);
}
}
}
return true;
}
bool texture_to_painting(
const TexturedMesh& textured,
PaintedMesh& painted,
const TexturePaintingSettings& settings,
PaintProgressCallback progress,
PaintCancelCallback cancel)
{
if (textured.vertices.empty() || textured.indices.empty() || textured.textures.empty())
return false;
cv::Mat texture;
tex2color::TriMesh input_mesh;
std::vector<std::vector<Vec2f>> uv_coords;
const bool multi_tex = textured.textures.size() > 1 && !textured.material_texture_map.empty();
if (multi_tex) {
if (!build_multi_texture_atlas(textured, texture, uv_coords))
return false;
// Build mesh geometry (atlas UVs already computed above)
const size_t nv = textured.vertices.size();
const size_t nf = textured.indices.size();
input_mesh.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i)
input_mesh.vertices[i] = Vec3f(
textured.vertices[i][0], textured.vertices[i][1], textured.vertices[i][2]);
input_mesh.indices.resize(nf);
for (size_t i = 0; i < nf; ++i)
input_mesh.indices[i] = Vec3i32(
textured.indices[i][0], textured.indices[i][1], textured.indices[i][2]);
} else {
texture = decode_texture_image(textured.textures[0]);
if (texture.empty())
return false;
build_tex2color_mesh(textured, input_mesh, uv_coords);
}
tex2color::TextureToColorSettings algo_settings;
algo_settings.target_colors_num = settings.target_colors_num;
algo_settings.smooth_weight = settings.smooth_weight;
algo_settings.oversampling_iters = settings.oversampling_iters;
switch (settings.mesh_repair_decision) {
case TexturePaintingSettings::MeshRepairDecision::Ask:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask;
break;
case TexturePaintingSettings::MeshRepairDecision::RepairAndImport:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport;
break;
case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair:
default:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair;
break;
}
tex2color::AlgoProgressCallback algo_progress = nullptr;
if (progress) {
algo_progress = [&progress](tex2color::AlgoProgress p) {
progress(p.percent, p.message);
};
}
tex2color::AlgoCancelCallback algo_cancel = nullptr;
if (cancel) {
algo_cancel = [&cancel]() -> bool { return cancel(); };
}
tex2color::TriMesh color_mesh;
std::vector<std::array<std::size_t,3>> face_colors;
algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required;
algo_settings.mesh_repair_callback = settings.mesh_repair_callback;
bool ok = tex2color::TextureToColor(
input_mesh, uv_coords, texture,
color_mesh, face_colors,
algo_settings, algo_progress, algo_cancel);
if (!ok)
return false;
extract_painted_mesh(color_mesh, face_colors, painted);
return true;
}
bool face_colors_to_painting(
const TexturedMesh& mesh,
PaintedMesh& painted,
const TexturePaintingSettings& settings,
PaintProgressCallback progress,
PaintCancelCallback cancel)
{
if (mesh.vertices.empty() || mesh.indices.empty() || mesh.precomputed_face_colors.empty())
return false;
// Build tex2color::TriMesh from input geometry
tex2color::TriMesh input_mesh;
input_mesh.vertices.resize(mesh.vertices.size());
for (size_t i = 0; i < mesh.vertices.size(); ++i)
input_mesh.vertices[i] = Vec3f(mesh.vertices[i][0], mesh.vertices[i][1], mesh.vertices[i][2]);
input_mesh.indices.resize(mesh.indices.size());
for (size_t i = 0; i < mesh.indices.size(); ++i)
input_mesh.indices[i] = Vec3i32(mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2]);
// Forward settings to tex2color
tex2color::TextureToColorSettings algo_settings;
algo_settings.target_colors_num = settings.target_colors_num;
algo_settings.smooth_weight = settings.smooth_weight;
switch (settings.mesh_repair_decision) {
case TexturePaintingSettings::MeshRepairDecision::Ask:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask;
break;
case TexturePaintingSettings::MeshRepairDecision::RepairAndImport:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport;
break;
case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair:
default:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair;
break;
}
algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required;
algo_settings.mesh_repair_callback = settings.mesh_repair_callback;
tex2color::AlgoProgressCallback algo_progress = nullptr;
if (progress) {
algo_progress = [&progress](tex2color::AlgoProgress p) {
progress(p.percent, p.message);
};
}
tex2color::AlgoCancelCallback algo_cancel = nullptr;
if (cancel) {
algo_cancel = [&cancel]() -> bool { return cancel(); };
}
tex2color::TriMesh out_mesh;
std::vector<std::array<std::size_t,3>> out_face_colors;
bool ok = tex2color::ClusterAndSmooth(
input_mesh, mesh.precomputed_face_colors, out_mesh, out_face_colors,
algo_settings, algo_progress, algo_cancel,
mesh.precomputed_vertex_colors);
if (!ok)
return false;
extract_painted_mesh(out_mesh, out_face_colors, painted);
return true;
}
double compute_delta_e(
const std::array<std::size_t,3>& rgb1,
const std::array<float,4>& rgba2)
{
return tex2color::color_utils::calc_rgb_color_difference_by_ciede2000(
rgb1,
{
static_cast<std::size_t>(rgba2[0] * 255.0f),
static_cast<std::size_t>(rgba2[1] * 255.0f),
static_cast<std::size_t>(rgba2[2] * 255.0f)
});
}
std::vector<FilamentMatch> match_clusters_to_filaments(
const std::vector<std::array<std::size_t,3>>& cluster_colors,
const std::vector<std::array<float,4>>& filament_colors,
const std::vector<std::string>& /*filament_names*/)
{
std::vector<FilamentMatch> matches(cluster_colors.size());
for (size_t ci = 0; ci < cluster_colors.size(); ++ci) {
matches[ci].cluster_index = static_cast<int>(ci);
matches[ci].cluster_color = cluster_colors[ci];
matches[ci].delta_e = 1e9;
for (size_t fi = 0; fi < filament_colors.size(); ++fi) {
double de = compute_delta_e(cluster_colors[ci], filament_colors[fi]);
if (de < matches[ci].delta_e) {
matches[ci].delta_e = de;
matches[ci].filament_index = static_cast<int>(fi);
matches[ci].filament_color = filament_colors[fi];
}
}
}
return matches;
}
bool apply_painted_mesh_to_volume(
const PaintedMesh& painted,
const std::vector<FilamentMatch>& matches,
ModelVolume& volume)
{
if (painted.face_colors.empty() || matches.empty())
return false;
const auto& cluster_colors = painted.cluster_colors;
std::map<std::array<std::size_t,3>, int> color_to_filament;
for (const auto& m : matches) {
if (m.cluster_index >= 0 && m.cluster_index < (int)cluster_colors.size() && m.filament_index >= 0)
color_to_filament[cluster_colors[m.cluster_index]] = m.filament_index;
}
indexed_triangle_set its;
its.vertices.resize(painted.vertices.size());
for (size_t i = 0; i < painted.vertices.size(); ++i) {
its.vertices[i] = Vec3f(
painted.vertices[i][0],
painted.vertices[i][1],
painted.vertices[i][2]);
}
its.indices.resize(painted.indices.size());
for (size_t i = 0; i < painted.indices.size(); ++i) {
its.indices[i] = Vec3i32(
painted.indices[i][0],
painted.indices[i][1],
painted.indices[i][2]);
}
TriangleMesh new_mesh(std::move(its));
// The volume already went through ModelObject::add_volume ->
// center_geometry_after_creation, which translated its mesh by
// -source.mesh_offset (and folded that shift into the volume
// transformation). The painted mesh, however, is derived from the
// raw textured mesh and is therefore expressed in the original
// un-centered coordinate frame. Reuse the exact recorded shift to
// align it -- do NOT compute it from the bounding-box centers of
// the two meshes: tex2color::TextureToColor performs subdivision
// and CGAL polygon-soup repair, so the painted vertex count and
// bbox no longer match the original textured mesh and a bbox-
// center alignment would silently displace the geometry.
//
// If the model has been scaled by Model::convert_from_meters /
// convert_from_imperial_units after load, the painted mesh fed
// here is already in millimetres (Model::convert_* also scales
// texture_mesh in place) while source.mesh_offset was recorded
// before the conversion and therefore still lives in the original
// pre-scaled frame. Bring it into the same frame as the painted
// vertices so the alignment shift below stays correct on the
// textured-import path. This compensation is scoped to this
// function so that other (non-textured) import paths are not
// affected.
Vec3d mesh_offset = volume.source.mesh_offset;
double unit_scale = 1.0;
if (volume.source.is_converted_from_meters)
unit_scale = 1000.0;
else if (volume.source.is_converted_from_inches)
unit_scale = 25.4;
if (unit_scale != 1.0)
mesh_offset *= unit_scale;
if (!mesh_offset.isApprox(Vec3d::Zero()))
new_mesh.translate(-mesh_offset.cast<float>());
new_mesh.set_init_shift(mesh_offset);
// Log bbox drift for diagnostics. Subdivision + CGAL polygon-soup
// repair routinely changes vertex count and bbox, so moderate drift
// is expected and must not block the apply.
if (!new_mesh.empty() && !volume.mesh().empty()) {
const Vec3d new_center = new_mesh.bounding_box().center();
const Vec3d cur_center = volume.mesh().bounding_box().center();
const double diag = volume.mesh().bounding_box().size().norm();
const double drift = (new_center - cur_center).norm();
if (drift > 0.05 * std::max(1.0, diag))
BOOST_LOG_TRIVIAL(warning)
<< "apply_painted_mesh_to_volume: painted bbox center drifted by "
<< drift << " (bbox diag=" << diag
<< ", unit_scale=" << unit_scale
<< ", from_meters=" << volume.source.is_converted_from_meters
<< ", from_inches=" << volume.source.is_converted_from_inches << ")";
else if (drift > 1e-3 * std::max(1.0, diag))
BOOST_LOG_TRIVIAL(info)
<< "apply_painted_mesh_to_volume: minor bbox drift "
<< drift << " (bbox diag=" << diag
<< ", unit_scale=" << unit_scale << ")";
}
volume.set_mesh(std::move(new_mesh));
volume.calculate_convex_hull();
// Re-center the replaced mesh so its bbox center sits at the origin,
// matching what center_geometry_after_creation did for the original mesh.
// CGAL repair / subdivision may shift the bbox center (drift); without
// re-centering, the volume offset (which was computed for the original
// centered mesh) no longer matches, causing the model to float or clip.
// Pass false to keep source.mesh_offset unchanged.
volume.center_geometry_after_creation(false);
volume.invalidate_convex_hull_2d();
// Mesh geometry has been replaced; any per-face annotation indexed
// against the previous triangle set is now stale. mmu_segmentation_facets
// is rewritten below from the new selector; reset the others so future
// import paths that carry support / seam / fuzzy_skin painting cannot
// leak indices from the old mesh into the new one.
volume.supported_facets.reset();
volume.fuzzy_skin_facets.reset();
volume.seam_facets.reset();
if (ModelObject* obj = volume.get_object())
obj->invalidate_bounding_box();
TriangleSelector selector(volume.mesh());
for (size_t fi = 0; fi < painted.face_colors.size() && fi < (size_t)volume.mesh().its.indices.size(); ++fi) {
auto it = color_to_filament.find(painted.face_colors[fi]);
if (it != color_to_filament.end()) {
int extruder_idx = it->second;
auto state = static_cast<EnforcerBlockerType>(
static_cast<int>(EnforcerBlockerType::Extruder1) + extruder_idx);
if (state <= EnforcerBlockerType::ExtruderMax)
selector.set_facet(static_cast<int>(fi), state);
}
}
volume.mmu_segmentation_facets.set(selector);
return true;
}
bool decode_texture_to_pixels(
const TextureImage& img,
std::vector<unsigned char>& out_pixels,
int& out_w, int& out_h)
{
cv::Mat decoded = decode_texture_image(img);
if (decoded.empty())
return false;
// decoded is BGR, CV_8UC3
out_w = decoded.cols;
out_h = decoded.rows;
size_t nbytes = (size_t)out_w * out_h * 3;
out_pixels.resize(nbytes);
if (decoded.isContinuous()) {
std::memcpy(out_pixels.data(), decoded.data, nbytes);
} else {
for (int r = 0; r < out_h; ++r)
std::memcpy(out_pixels.data() + r * out_w * 3, decoded.ptr(r), out_w * 3);
}
return true;
}
// Sample face color from texture using 3 explicit UV values (centroid + bilinear).
static std::array<std::size_t,3> sample_face_from_uvs(
const cv::Mat& tex,
const std::array<float,2>& uv0,
const std::array<float,2>& uv1,
const std::array<float,2>& uv2)
{
float cu = (uv0[0] + uv1[0] + uv2[0]) / 3.f;
float cv_val = (uv0[1] + uv1[1] + uv2[1]) / 3.f;
cu = cu - std::floor(cu);
cv_val = cv_val - std::floor(cv_val);
float fx = cu * (tex.cols - 1);
float fy = cv_val * (tex.rows - 1);
int x0 = std::clamp(static_cast<int>(fx), 0, tex.cols - 1);
int y0 = std::clamp(static_cast<int>(fy), 0, tex.rows - 1);
int x1 = std::min(x0 + 1, tex.cols - 1);
int y1 = std::min(y0 + 1, tex.rows - 1);
float wx = fx - x0;
float wy = fy - y0;
const int ch = tex.channels();
auto sample = [&](int row, int col) -> std::array<float,3> {
const uchar* ptr = tex.data + row * tex.step[0] + col * ch;
return {static_cast<float>(ptr[2]), static_cast<float>(ptr[1]), static_cast<float>(ptr[0])};
};
auto c00 = sample(y0, x0);
auto c10 = sample(y0, x1);
auto c01 = sample(y1, x0);
auto c11 = sample(y1, x1);
std::array<std::size_t,3> color;
for (int i = 0; i < 3; ++i) {
float top = c00[i] * (1.f - wx) + c10[i] * wx;
float bot = c01[i] * (1.f - wx) + c11[i] * wx;
color[i] = static_cast<std::size_t>(std::clamp(top * (1.f - wy) + bot * wy, 0.f, 255.f));
}
return color;
}
// Legacy overload: look up UVs from per-vertex array by vertex indices.
static std::array<std::size_t,3> sample_face_from_texture(
const cv::Mat& tex,
const std::vector<std::array<float,2>>& uvs,
const std::array<int,3>& face)
{
std::array<float,2> uv0 = {0.f, 0.f}, uv1 = {0.f, 0.f}, uv2 = {0.f, 0.f};
if (face[0] >= 0 && static_cast<size_t>(face[0]) < uvs.size()) uv0 = uvs[face[0]];
if (face[1] >= 0 && static_cast<size_t>(face[1]) < uvs.size()) uv1 = uvs[face[1]];
if (face[2] >= 0 && static_cast<size_t>(face[2]) < uvs.size()) uv2 = uvs[face[2]];
return sample_face_from_uvs(tex, uv0, uv1, uv2);
}
bool sample_original_face_colors(
const TexturedMesh& textured,
std::vector<std::array<std::size_t,3>>& out_face_colors)
{
if (textured.indices.empty())
return false;
// Decode all textures up front
std::vector<cv::Mat> decoded_textures;
decoded_textures.reserve(textured.textures.size());
for (const auto& ti : textured.textures) {
decoded_textures.push_back(decode_texture_image(ti));
}
const bool has_mapping = !textured.material_texture_map.empty();
const size_t nf = textured.indices.size();
out_face_colors.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
int tex_idx = -1;
if (has_mapping && mat_idx >= 0 && static_cast<size_t>(mat_idx) < textured.material_texture_map.size())
tex_idx = textured.material_texture_map[mat_idx];
else if (!decoded_textures.empty())
tex_idx = 0; // fallback: single-texture model
if (tex_idx >= 0 && static_cast<size_t>(tex_idx) < decoded_textures.size()
&& !decoded_textures[tex_idx].empty()) {
if (textured.has_face_uvs()) {
const auto& ui = textured.uv_indices[fi];
auto get_uv = [&](int vi) -> std::array<float,2> {
int idx = ui[vi];
if (idx >= 0 && static_cast<size_t>(idx) < textured.uv_coords.size())
return textured.uv_coords[idx];
return {0.f, 0.f};
};
out_face_colors[fi] = sample_face_from_uvs(
decoded_textures[tex_idx], get_uv(0), get_uv(1), get_uv(2));
} else {
out_face_colors[fi] = sample_face_from_texture(
decoded_textures[tex_idx], textured.uvs, textured.indices[fi]);
}
} else if (has_mapping && mat_idx >= 0
&& static_cast<size_t>(mat_idx) < textured.material_colors.size()) {
// No texture — use baseColorFactor as solid color
const auto& c = textured.material_colors[mat_idx];
out_face_colors[fi] = {
static_cast<std::size_t>(std::clamp(c[0] * 255.f, 0.f, 255.f)),
static_cast<std::size_t>(std::clamp(c[1] * 255.f, 0.f, 255.f)),
static_cast<std::size_t>(std::clamp(c[2] * 255.f, 0.f, 255.f))
};
} else {
out_face_colors[fi] = {192, 192, 192}; // default gray
}
}
return true;
}
} // namespace Slic3r
+137
View File
@@ -0,0 +1,137 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include <vector>
struct indexed_triangle_set;
namespace Slic3r {
class TriangleMesh;
class ModelVolume;
struct TextureImage {
int width = 0;
int height = 0;
int channels = 4;
std::vector<unsigned char> data;
};
struct TexturedMesh {
std::vector<std::array<float,3>> vertices;
std::vector<std::array<int,3>> indices;
std::vector<std::array<float,2>> uvs;
std::vector<TextureImage> textures;
std::vector<int> material_ids;
// material index -> index in textures[] (-1 if no texture, use material_colors)
std::vector<int> material_texture_map;
// per-material baseColorFactor (RGBA 0-1), indexed by material index
std::vector<std::array<float,4>> material_colors;
// Per-face independent UV support (for OBJ where the same vertex can have
// different texture coordinates on different faces).
std::vector<std::array<float,2>> uv_coords; // UV coordinate pool
std::vector<std::array<int,3>> uv_indices; // per-face UV indices into uv_coords
bool has_face_uvs() const { return !uv_indices.empty() && !uv_coords.empty(); }
// Pre-computed per-face colors (e.g. from OBJ vertex colors or MTL Kd).
// When non-empty, the pipeline skips texture decode/sample/oversample and
// consumes these instead of sampling a texture.
// Each entry is {R, G, B} in [0..255].
std::vector<std::array<std::size_t,3>> precomputed_face_colors;
// Per-vertex colors from OBJ (RGBA, [0..1]), indexed by vertex index.
// On a low-poly mesh these are quantized into a small palette and the mesh is
// split along the resulting cluster boundaries, so color borders stay sharp
// instead of being averaged away into a single color per face.
std::vector<std::array<float,4>> precomputed_vertex_colors;
};
struct PaintedMesh {
std::vector<std::array<float,3>> vertices;
std::vector<std::array<int,3>> indices;
std::vector<std::array<std::size_t,3>> face_colors; // per-face RGB [0..255]
std::vector<std::array<std::size_t,3>> cluster_colors;
};
using PaintProgressCallback = std::function<void(int percent, const char* message)>;
using PaintCancelCallback = std::function<bool()>;
using PaintMeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh,
indexed_triangle_set& repaired_mesh,
std::function<void(const char* message, unsigned progress)> progress_callback,
std::function<bool()> cancel_callback,
std::string* error_message)>;
struct TexturePaintingSettings {
std::size_t target_colors_num = 4;
double smooth_weight = 0.5;
std::size_t oversampling_iters = 0;
enum class MeshRepairDecision {
Ask,
ImportWithoutRepair,
RepairAndImport
};
MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair;
bool* mesh_repair_decision_required = nullptr;
PaintMeshRepairCallback mesh_repair_callback;
};
struct FilamentMatch {
int cluster_index = -1;
int filament_index = -1;
double delta_e = 0.0;
std::array<std::size_t,3> cluster_color = {0,0,0};
std::array<float,4> filament_color = {0,0,0,1};
};
bool texture_to_painting(
const TexturedMesh& textured,
PaintedMesh& painted,
const TexturePaintingSettings& settings = {},
PaintProgressCallback progress = nullptr,
PaintCancelCallback cancel = nullptr);
// Turn pre-computed per-face colors into a painted mesh, skipping texture decode
// and UV sampling. A low-poly mesh that also carries precomputed_vertex_colors is
// split along quantized color boundaries, which replaces its geometry.
bool face_colors_to_painting(
const TexturedMesh& mesh,
PaintedMesh& painted,
const TexturePaintingSettings& settings = {},
PaintProgressCallback progress = nullptr,
PaintCancelCallback cancel = nullptr);
std::vector<FilamentMatch> match_clusters_to_filaments(
const std::vector<std::array<std::size_t,3>>& cluster_colors,
const std::vector<std::array<float,4>>& filament_colors,
const std::vector<std::string>& filament_names);
double compute_delta_e(
const std::array<std::size_t,3>& rgb1,
const std::array<float,4>& rgba2);
bool apply_painted_mesh_to_volume(
const PaintedMesh& painted,
const std::vector<FilamentMatch>& matches,
ModelVolume& volume);
// Decode a TextureImage (which may contain raw PNG/JPEG bytes) into BGR pixel data.
// On success, populates out_pixels (BGR, 3 bytes/pixel) and sets out_w/out_h.
bool decode_texture_to_pixels(
const TextureImage& img,
std::vector<unsigned char>& out_pixels,
int& out_w, int& out_h);
// Sample per-face colors from the correct texture per material_ids.
// Uses material_texture_map / material_colors for multi-material GLBs.
// Falls back to textures[0] when the mapping is absent.
bool sample_original_face_colors(
const TexturedMesh& textured,
std::vector<std::array<std::size_t,3>>& out_face_colors);
} // namespace Slic3r
@@ -0,0 +1,15 @@
#pragma once
#include <functional>
namespace Slic3r { namespace tex2color {
struct AlgoProgress {
int percent = 0;
const char* message = "";
};
using AlgoProgressCallback = std::function<void(AlgoProgress)>;
using AlgoCancelCallback = std::function<bool()>;
} // namespace tex2color
} // namespace Slic3r
+173
View File
@@ -0,0 +1,173 @@
#pragma once
#include "TriMesh.hpp"
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Polygon_mesh_processing/repair.h>
#include <chrono>
#include <cstdio>
#include <map>
#include <queue>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace Slic3r { namespace tex2color {
namespace cgalutils {
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using CGALMesh = CGAL::Surface_mesh<Kernel::Point_3>;
inline CGALMesh trimesh_to_cgal(const TriMesh& mesh) {
CGALMesh cm;
std::vector<CGALMesh::Vertex_index> vmap(mesh.vertices.size());
for (size_t i = 0; i < mesh.vertices.size(); ++i)
vmap[i] = cm.add_vertex(Kernel::Point_3(mesh.vertices[i].x(), mesh.vertices[i].y(), mesh.vertices[i].z()));
for (const auto& f : mesh.indices) {
cm.add_face(vmap[f[0]], vmap[f[1]], vmap[f[2]]);
}
return cm;
}
inline TriMesh cgal_to_trimesh(const CGALMesh& cm) {
TriMesh mesh;
std::map<CGALMesh::Vertex_index, size_t> vmap;
size_t idx = 0;
for (auto v : cm.vertices()) {
if (!cm.is_valid(v) || cm.is_removed(v)) continue;
auto p = cm.point(v);
mesh.vertices.push_back(Vec3f((float)p.x(), (float)p.y(), (float)p.z()));
vmap[v] = idx++;
}
for (auto f : cm.faces()) {
if (!cm.is_valid(f) || cm.is_removed(f)) continue;
auto h = cm.halfedge(f);
auto v0 = cm.target(h);
auto v1 = cm.target(cm.next(h));
auto v2 = cm.target(cm.next(cm.next(h)));
mesh.indices.push_back(Vec3i32((int)vmap[v0], (int)vmap[v1], (int)vmap[v2]));
}
return mesh;
}
inline bool is_mesh_halfedge_compatible(const TriMesh& mesh) {
std::vector<std::unordered_set<std::size_t>> vtx_to_adj_faces(mesh.vertices.size());
std::size_t edge_id = 0;
std::vector<std::unordered_set<std::size_t>> edge_to_faces;
std::vector<std::unordered_set<std::size_t>> vtx_to_prev_vtxs(mesh.vertices.size());
std::vector<std::unordered_set<std::size_t>> vtx_to_next_vtxs(mesh.vertices.size());
std::vector<std::unordered_map<std::size_t, std::size_t>> vtx_vtx_to_edge(mesh.vertices.size());
for (std::size_t fid = 0; fid < mesh.indices.size(); ++fid) {
const TriFace& face = mesh.indices[fid];
if (face[0] == face[1] || face[1] == face[2] || face[2] == face[0]) {
return false;
}
for (std::size_t i = 0; i < 3; ++i) {
if (static_cast<std::size_t>(face[i]) >= mesh.vertices.size()) {
return false;
}
vtx_to_adj_faces[face[i]].insert(fid);
std::size_t prev_vtx = face[(i + 2) % 3];
std::size_t next_vtx = face[(i + 1) % 3];
if (vtx_to_prev_vtxs[face[i]].count(prev_vtx)) {
return false;
}
vtx_to_prev_vtxs[face[i]].insert(prev_vtx);
if (vtx_to_next_vtxs[face[i]].count(next_vtx)) {
return false;
}
vtx_to_next_vtxs[face[i]].insert(next_vtx);
}
for (std::size_t i = 0; i < 3; ++i) {
std::size_t va = face[i];
std::size_t vb = face[(i + 1) % 3];
if (!vtx_vtx_to_edge[va].count(vb)) {
vtx_vtx_to_edge[va][vb] = edge_id;
vtx_vtx_to_edge[vb][va] = edge_id;
++edge_id;
edge_to_faces.emplace_back(std::unordered_set<std::size_t>());
}
edge_to_faces[vtx_vtx_to_edge[va][vb]].insert(fid);
}
}
for (std::size_t vid = 0; vid < mesh.vertices.size(); ++vid) {
if (vtx_to_adj_faces[vid].empty()) {
continue;
}
std::unordered_set<std::size_t> visited_faces;
std::queue<std::size_t> face_queue;
face_queue.push(*(vtx_to_adj_faces[vid].begin()));
visited_faces.insert(*(vtx_to_adj_faces[vid].begin()));
while (!face_queue.empty()) {
std::size_t fid = face_queue.front();
face_queue.pop();
const TriFace& face = mesh.indices[fid];
for (std::size_t i = 0; i < 3; ++i) {
if (static_cast<std::size_t>(face[i]) != vid) {
continue;
}
std::size_t v_next = face[(i + 1) % 3];
std::size_t v_prev = face[(i + 2) % 3];
for (std::size_t nbr : {v_next, v_prev}) {
std::size_t eid = vtx_vtx_to_edge[vid][nbr];
for (std::size_t adj_fid : edge_to_faces[eid]) {
if (!visited_faces.count(adj_fid) && vtx_to_adj_faces[vid].count(adj_fid)) {
visited_faces.insert(adj_fid);
face_queue.push(adj_fid);
}
}
}
break;
}
}
for (std::size_t fid : vtx_to_adj_faces[vid]) {
if (!visited_faces.count(fid)) {
return false;
}
}
}
return true;
}
inline bool convert_trimesh_to_cgal(const TriMesh& mesh, CGALMesh& cgal_mesh) {
cgal_mesh = trimesh_to_cgal(mesh);
return cgal_mesh.number_of_faces() > 0 || mesh.indices.empty();
}
inline bool convert_trimesh_to_cgal(
const TriMesh& mesh, const std::vector<Vec2f>& vertex_uvs,
CGALMesh& cgal_mesh, std::vector<Vec2f>& cgal_vertex_uvs)
{
cgal_mesh.clear();
std::vector<CGALMesh::Vertex_index> vmap(mesh.vertices.size());
cgal_vertex_uvs.clear();
for (size_t i = 0; i < mesh.vertices.size(); ++i) {
vmap[i] = cgal_mesh.add_vertex(Kernel::Point_3(
mesh.vertices[i].x(), mesh.vertices[i].y(), mesh.vertices[i].z()));
}
cgal_vertex_uvs.resize(cgal_mesh.num_vertices());
for (size_t i = 0; i < mesh.vertices.size(); ++i) {
if (i < vertex_uvs.size())
cgal_vertex_uvs[vmap[i]] = vertex_uvs[i];
else
cgal_vertex_uvs[vmap[i]] = Vec2f(0.f, 0.f);
}
for (const auto& f : mesh.indices)
cgal_mesh.add_face(vmap[f[0]], vmap[f[1]], vmap[f[2]]);
return true;
}
} // namespace cgalutils
} // namespace tex2color
} // namespace Slic3r
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
#pragma once
#include "Callbacks.hpp"
#include "TriMesh.hpp"
namespace Slic3r { namespace tex2color {
namespace color_utils {
struct ClusterParameters;
typedef std::array<std::size_t, 3> Color; // RGB: [R, G, B] 0~255
typedef std::vector<Color> ColorList;
typedef std::array<double, 3> ColorDouble;
typedef std::array<std::size_t, 3> RGB;
// Function pointer type that points to a specific color-difference function based on the chosen method.
using DistanceFunction = double (*)(const Color&, const Color&);
// Color space used for computing color differences.
enum struct ColorDifferenceMethod : std::size_t {
RGB = 0, // Simplest and fastest
Lab = 1 // Most perceptually accurate
};
struct ClusterParameters {
ColorDifferenceMethod color_difference_method = ColorDifferenceMethod::Lab; // Method for measuring color difference; Lab is the most accurate
double max_color_distance = 25; // Max intra-cluster radius (CIEDE2000 dE) for adaptive clustering; ignored by the fixed-K algorithm
std::size_t cluster_k = 10; // Target number of cluster centers; ignored by the adaptive algorithm
std::size_t max_cluster_k = 32; // Max cluster count upper bound for adaptive algorithm
std::size_t max_iter = 50; // Maximum number of iterations
std::function<bool()> cancel_callback; // Optional cancellation check; returns true when the caller requests abort
};
struct SmoothParameters {
double smooth_weight = 0.5; // Controls smoothing intensity; larger values produce smoother results. Range: [0.0, 1.0]
};
/**
* @brief Compute the squared Euclidean distance between two RGB colors.
*
* @param[in] rgb1 First RGB color [R, G, B], range 0~255.
* @param[in] rgb2 Second RGB color [R, G, B], range 0~255.
* @return Squared Euclidean distance: (R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2.
*/
double calc_rgb_color_difference_by_squared_rgb(const RGB& rgb1, const RGB& rgb2);
/**
* @brief Compute the squared Euclidean distance between two RGB colors (double precision).
*
* @param[in] c1 First RGB color [R, G, B], as double.
* @param[in] c2 Second RGB color [R, G, B], as double.
* @return Squared Euclidean distance: (R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2.
*/
double calc_rgb_color_difference_by_squared_rgb_double(const ColorDouble& c1, const ColorDouble& c2);
/**
* @brief Compute the CIEDE2000 color difference between two RGB colors.
*
* Currently the most accurate color-difference formula, recommended by CIE as the industry standard.
* - dE <= 1.0: imperceptible to the human eye, high-precision color matching.
* - dE <= 2.0: slight difference, noticeable by experts; printing / image processing standard.
* - dE <= 3.0: noticeable by ordinary observers; general quality control.
*
* @param[in] rgb1 First RGB color [R, G, B], range 0~255.
* @param[in] rgb2 Second RGB color [R, G, B], range 0~255.
* @return CIEDE2000 color difference; smaller values indicate more similar colors.
*/
double calc_rgb_color_difference_by_ciede2000(const RGB& rgb1, const RGB& rgb2);
/**
* @brief Compute the CIEDE2000 color difference between two sRGB colors (double precision, non-linear channels in [0,1]).
*
* Uses the same XYZ/Lab/dE00 pipeline as calc_rgb_color_difference_by_ciede2000 but without uint8
* quantization or the intermediate x255 conversion; suitable for bisection, color blending, and other
* iterative scenarios. Note: ColorDouble here represents [R,G,B] in [0,1], which differs from the
* 0~255 scale used by other interfaces in this file. Callers should follow the naming convention.
*
* @param[in] rgb1 rgb2 sRGB non-linear channel values, recommended range [0,1].
*/
double calc_rgb_color_difference_by_ciede2000_srgb01(const ColorDouble& rgb1, const ColorDouble& rgb2);
/**
* @brief K-Means clustering algorithm that minimizes the sum of squared errors.
*
* Uses K-Means++ initialization to iteratively find the optimal cluster centers.
*
* @param[in] colors Input color list.
* @param[in] cluster_parameters Clustering parameters including cluster count, max iterations, color-difference method, etc.
* @return List of cluster-center colors whose size equals cluster_parameters.cluster_k.
*/
std::vector<Color> cluster_k_means(const std::vector<Color>& colors, const ClusterParameters& cluster_parameters);
/**
* @brief Adaptive K-Means clustering that determines an appropriate number of clusters under a max color-distance constraint.
*
* Automatically finds the optimal cluster count via binary search so that max_color_distance is satisfied.
*
* @param[in] colors Input color list.
* @param[in] cluster_parameters Clustering parameters; cluster_k is ignored and determined automatically.
* @return List of cluster-center colors whose count is determined by the algorithm based on max_color_distance.
*/
std::vector<Color> cluster_adaptive(const std::vector<Color>& colors, const ClusterParameters& cluster_parameters);
/**
* @brief Cluster a color list to a set of specified cluster centers.
*
* For each input color, find the nearest specified cluster center and replace it.
*
* @param[in] colors Input color list.
* @param[in] specified_colors Specified cluster-center colors.
* @return Clustered color list where each color is replaced by its nearest center.
*/
std::vector<Color> cluster_to_specified_colors(const std::vector<Color>& colors, const std::vector<Color>& specified_colors);
/**
* @brief Remesh the mesh while preserving color boundaries.
*
* Performs isotropic remeshing while protecting color boundaries. Edges whose two adjacent
* faces have different colors are marked as feature edges and will not be modified.
*
* @param[in,out] mesh Input mesh; modified in-place after remeshing.
* @param[in,out] face_labels Face color labels; updated to match the new mesh.
* @param[in] target_edge_length_ratio Ratio of target average edge length to input average edge length; >1 simplifies, <1 refines.
* @return true on success, false on failure.
*/
bool remesh_mesh(TriMesh& mesh, std::vector<std::size_t>& face_labels, double target_edge_length_ratio);
/**
* @brief Check whether the mesh is closed (watertight).
*
* A mesh is closed if it has no boundary edges, i.e. every edge is shared by exactly two faces.
*
* @param[in] tri_mesh Input mesh.
* @return true if the mesh is closed, false if it has boundary edges.
*/
bool is_closed(const TriMesh& tri_mesh);
/**
* @brief Smooth region boundaries (RGB color labels).
*
* Applies topological smoothing (label reassignment) and geometric smoothing (boundary vertex relocation).
*
* @param[in,out] tri_mesh Input mesh; modified in-place after smoothing.
* @param[in,out] face_labels Face color labels (RGB format); updated after smoothing.
* @param[in] smooth_parameters Smoothing control parameters.
* @return true on success, false on failure.
*/
bool smooth_region(TriMesh& tri_mesh, std::vector<std::array<std::size_t, 3>>& face_labels, const SmoothParameters& smooth_parameters = SmoothParameters());
/**
* @brief Smooth region boundaries (integer labels).
*
* Applies topological smoothing (label reassignment) and geometric smoothing (boundary vertex relocation).
*
* @param[in,out] tri_mesh Input mesh; modified in-place after smoothing.
* @param[in,out] face_labels Integer face labels; updated after smoothing.
* @param[in] smooth_parameters Smoothing control parameters.
* @return true on success, false on failure.
*/
bool smooth_region(TriMesh& tri_mesh, std::vector<std::size_t>& face_labels, const SmoothParameters& smooth_parameters = SmoothParameters());
/**
* @brief Split the mesh into connected components.
*
* Based on face connectivity, the mesh is split into independent components, each forming a
* standalone mesh. Texture coordinates for each component are preserved.
*
* @param[in] mesh Input mesh.
* @param[in] vertex_uvs Vertex texture coordinates.
* @param[out] component_meshes Output list of component meshes.
* @param[out] component_vertex_uvs Output list of texture coordinates per component.
* @return true on success, false on failure.
*/
bool get_components(const TriMesh& mesh, const std::vector<Vec2f>& vertex_uvs, std::vector<TriMesh>& component_meshes,
std::vector<std::vector<Vec2f>>& component_vertex_uvs);
/**
* @brief Find the ID of the nearest color in a color list to a given color.
*
* @param[in] colors Color list.
* @param[in] color Target color.
* @param[out] nearest_color_id ID of the nearest color found.
* @return true on success, false on failure.
*/
bool calc_nearest_color_id(const std::vector<RGB>& colors, const RGB& color, std::size_t& nearest_color_id);
/**
* @brief Cluster mesh face colors based on given cluster centers.
*
* @param[in] mesh Input mesh.
* @param[in] cluster_centers Cluster-center RGB colors.
* @param[in, out] map_face_to_rgb RGB color per face; updated to the nearest cluster center after clustering.
* @param[out] map_face_to_cluster_id Cluster-center ID per face; updated to the nearest cluster center ID.
* @return true on success, false on failure.
*/
bool mesh_cluster(const TriMesh& mesh, const std::vector<RGB>& cluster_centers, std::vector<RGB>& map_face_to_rgb,
std::vector<std::size_t>& map_face_to_cluster_id);
} // namespace color_utils
} // namespace tex2color
} // namespace Slic3r
+252
View File
@@ -0,0 +1,252 @@
#pragma once
#include "TriMesh.hpp"
#include "CgalUtils.hpp"
#include "Callbacks.hpp"
#include <CGAL/Polygon_mesh_processing/border.h>
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/repair.h>
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <memory>
#include <utility>
namespace Slic3r { namespace tex2color {
namespace PMP = CGAL::Polygon_mesh_processing;
// Default upper bound on the number of half-edges in any single boundary cycle
// that CloseBoundariesAndRepairManifoldness will attempt to triangulate. The
// cost of triangulate_hole grows non-linearly with cycle length, so this caps
// the worst-case per-hole work rather than the aggregate boundary size: a mesh
// with many small holes is still fully repaired, while a mesh containing one
// pathologically large hole skips triangulation entirely.
inline constexpr std::size_t MAX_REPAIRABLE_MESH_HOLE_EDGES = 500;
// Default upper bound on the aggregate number of boundary half-edges in the
// mesh (summed across every boundary cycle). When the total boundary length is
// excessive, even if each individual cycle is short, triangulating all of them
// usually indicates a severely fragmented input (e.g. heavily damaged scans)
// and rarely yields a usable result, so we skip hole closing entirely.
inline constexpr std::size_t MAX_REPAIRABLE_MESH_BOUNDARY_EDGES = 5000;
struct RepairSetting
{
// Skip triangulating a boundary cycle whose half-edge count exceeds this.
std::size_t max_hole_edges = MAX_REPAIRABLE_MESH_HOLE_EDGES;
// Skip hole closing entirely when the total boundary half-edge count
// (summed across all cycles) exceeds this.
std::size_t max_boundary_edges = MAX_REPAIRABLE_MESH_BOUNDARY_EDGES;
};
struct BoundaryEdgeStats
{
std::size_t total_boundary_edges = 0;
std::size_t max_cycle_edges = 0;
std::size_t cycle_count = 0;
};
// Read-only inspection of the mesh's boundary cycles. Caller is responsible for
// any pre-processing (e.g. stitch_borders) needed for the count to be meaningful.
inline BoundaryEdgeStats ComputeBoundaryEdgeStats(const cgalutils::CGALMesh& cgal_mesh)
{
using CGALMesh = cgalutils::CGALMesh;
using HalfedgeDescriptor = boost::graph_traits<CGALMesh>::halfedge_descriptor;
std::vector<HalfedgeDescriptor> border_cycles;
PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles));
BoundaryEdgeStats stats;
stats.cycle_count = border_cycles.size();
for (const HalfedgeDescriptor h0 : border_cycles) {
std::size_t len = 0;
HalfedgeDescriptor h = h0;
do {
++len;
h = next(h, cgal_mesh);
} while (h != h0);
stats.max_cycle_edges = std::max(stats.max_cycle_edges, len);
stats.total_boundary_edges += len;
}
return stats;
}
// Unconditionally close every boundary cycle of the mesh and repair non-manifold
// vertices. The caller (e.g. RepairMesh) is expected to gate this call based on
// boundary statistics; entering this function always triggers triangulation.
inline void CloseBoundariesAndRepairManifoldness(cgalutils::CGALMesh& cgal_mesh)
{
using CGALMesh = cgalutils::CGALMesh;
using HalfedgeDescriptor = boost::graph_traits<CGALMesh>::halfedge_descriptor;
using FaceDescriptor = boost::graph_traits<CGALMesh>::face_descriptor;
PMP::stitch_borders(cgal_mesh);
PMP::duplicate_non_manifold_vertices(cgal_mesh);
std::vector<HalfedgeDescriptor> border_cycles;
PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles));
for (const HalfedgeDescriptor h : border_cycles) {
std::vector<FaceDescriptor> patch_faces;
PMP::triangulate_hole(cgal_mesh, h, std::back_inserter(patch_faces));
}
PMP::remove_degenerate_faces(cgal_mesh);
PMP::duplicate_non_manifold_vertices(cgal_mesh);
}
inline bool RepairMesh(const TriMesh& mesh,
std::shared_ptr<TriMesh>& out_mesh,
AlgoProgressCallback progress_callback = nullptr,
AlgoCancelCallback cancel_callback = nullptr,
const RepairSetting& setting = RepairSetting{})
{
using Clock = std::chrono::steady_clock;
auto elapsed_ms = [](Clock::time_point t0) {
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
};
const Clock::time_point t_total = Clock::now();
// Convert TriMesh to polygon soup (point container + triangle index container)
std::vector<cgalutils::Kernel::Point_3> soup_points;
std::vector<std::vector<std::size_t>> soup_triangles;
soup_points.reserve(mesh.vertices.size());
for (const TriVertex& v : mesh.vertices) {
soup_points.emplace_back(v.x(), v.y(), v.z());
}
soup_triangles.reserve(mesh.indices.size());
for (const TriFace& f : mesh.indices) {
soup_triangles.push_back({static_cast<std::size_t>(f[0]),
static_cast<std::size_t>(f[1]),
static_cast<std::size_t>(f[2])});
}
if (progress_callback) {
progress_callback({30, "Repairing polygon soup"});
}
if (cancel_callback && cancel_callback()) {
return false;
}
{
const auto t0 = Clock::now();
PMP::repair_polygon_soup(soup_points, soup_triangles);
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=repair_polygon_soup took="
<< elapsed_ms(t0) << " ms";
}
if (progress_callback) {
progress_callback({50, "Orienting polygon soup"});
}
if (cancel_callback && cancel_callback()) {
return false;
}
{
const auto t0 = Clock::now();
PMP::orient_polygon_soup(soup_points, soup_triangles);
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=orient_polygon_soup took="
<< elapsed_ms(t0) << " ms";
}
if (progress_callback) {
progress_callback({70, "Converting to CGAL mesh"});
}
if (cancel_callback && cancel_callback()) {
return false;
}
cgalutils::CGALMesh cgal_mesh;
{
const auto t0 = Clock::now();
PMP::polygon_soup_to_polygon_mesh(soup_points, soup_triangles, cgal_mesh);
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=polygon_soup_to_polygon_mesh took="
<< elapsed_ms(t0) << " ms";
}
{
const auto t0 = Clock::now();
PMP::remove_degenerate_faces(cgal_mesh);
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=remove_degenerate_faces took="
<< elapsed_ms(t0) << " ms";
}
if (progress_callback) {
progress_callback({80, "Closing mesh boundaries"});
}
if (cancel_callback && cancel_callback()) {
return false;
}
// Stitch borders and duplicate non-manifold vertices first so that the
// boundary statistics below reflect the post-stitch topology; otherwise
// boundaries that would close on stitching inflate the counts and may
// cause the gate to skip hole filling unnecessarily.
BoundaryEdgeStats stats;
{
const auto t0 = Clock::now();
PMP::stitch_borders(cgal_mesh);
PMP::duplicate_non_manifold_vertices(cgal_mesh);
stats = ComputeBoundaryEdgeStats(cgal_mesh);
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=boundary_stats took="
<< elapsed_ms(t0) << " ms"
<< " total_boundary_edges=" << stats.total_boundary_edges
<< " max_cycle_edges=" << stats.max_cycle_edges
<< " cycle_count=" << stats.cycle_count;
}
const bool can_repair_holes =
stats.total_boundary_edges <= setting.max_boundary_edges &&
stats.max_cycle_edges <= setting.max_hole_edges;
if (can_repair_holes) {
const auto t0 = Clock::now();
CloseBoundariesAndRepairManifoldness(cgal_mesh);
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=close_boundaries took="
<< elapsed_ms(t0) << " ms";
} else {
BOOST_LOG_TRIVIAL(info)
<< "TextureToColor: RepairMesh skip hole closing"
<< ", total_boundary_edges=" << stats.total_boundary_edges
<< " (limit=" << setting.max_boundary_edges << ")"
<< ", max_cycle_edges=" << stats.max_cycle_edges
<< " (limit=" << setting.max_hole_edges << ")"
<< ", cycle_count=" << stats.cycle_count;
}
if (progress_callback) {
progress_callback({85, "Converting from CGAL mesh"});
}
if (cancel_callback && cancel_callback()) {
return false;
}
std::shared_ptr<TriMesh> out;
{
const auto t0 = Clock::now();
out = std::make_shared<TriMesh>(cgalutils::cgal_to_trimesh(cgal_mesh));
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=cgal_to_trimesh took="
<< elapsed_ms(t0) << " ms";
}
out_mesh = std::move(out);
if (progress_callback) {
progress_callback({100, "Done"});
}
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh total=" << elapsed_ms(t_total) << " ms";
return true;
}
} // namespace tex2color
} // namespace Slic3r
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,103 @@
#pragma once
#include "Callbacks.hpp"
#include "TriMesh.hpp"
#include "opencv2/core.hpp"
#include <functional>
#include <string>
namespace Slic3r { namespace tex2color {
enum class MeshRepairDecision {
Ask,
ImportWithoutRepair,
RepairAndImport
};
using MeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh,
indexed_triangle_set& repaired_mesh,
std::function<void(const char* message, unsigned progress)> progress_callback,
std::function<bool()> cancel_callback,
std::string* error_message)>;
struct TextureToColorSettings {
std::size_t target_colors_num = 4; // 目标颜色数量, 为0时, 自适应计算; 否则计算指定数目的颜色聚类
double smooth_weight = 0.5; // 光顺权重, 范围[0, 1], 0表示不进行光顺, 1表示完全光顺
// 当超采样迭代次数大于0时, 进行指定迭代次数的超采样; 否则, 自适应超采样
std::size_t oversampling_iters = 0; // 超采样迭代次数
std::size_t oversampling_min_face_count = 10000; // 自适应采样: 当face_count小于oversampling_min_face_count时, 进行超采样
std::size_t oversampling_max_face_count = 1000000; // 无论输入参数如何, 超采样后的面片数不能超过oversampling_max_face_count
double max_color_distance = 25.0; // 自适应聚类允许的最大簇内半径(CIEDE2000 ΔE)
std::size_t max_cluster_k = 32; // 自适应聚类的最大颜色数量上限
MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair;
// Set by TextureToColor when Ask is selected and mesh repair needs user confirmation.
bool* mesh_repair_decision_required = nullptr;
MeshRepairCallback mesh_repair_callback;
};
/**
* @brief 将纹理贴图转换为网格面片颜色, 并通过聚类和光顺生成可用于多色打印的着色网格
*
* 基于纹理网格的UV坐标对纹理图像进行采样, 计算每个面片的颜色,
* 然后对颜色进行聚类(K-Means或自适应)和区域光顺, 最终输出带颜色信息的网格
*
* @param[in] texture_mesh 带有UV坐标的输入三角网格
* @param[in] uv_coords 每个面片的UV坐标, 大小等于面片数, 每个面片有三个UV坐标
* @param[in] texture 纹理图像
* @param[out] color_mesh 输出的着色网格
* @param[out] face_colors 输出的着色网格的面片颜色, 大小等于面片数, 颜色值为[R, G, B], 范围0~255
* @param[in] settings 算法参数, 包括目标颜色数量、光顺权重等
* @param[in] progress_callback 进度回调函数
* @param[in] cancel_callback 取消回调函数
* @return 成功返回true, 输入数据无效(空网格、无UV、空纹理等)返回false
*/
bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<Vec2f>>& uv_coords, const cv::Mat& texture, TriMesh& color_mesh,
std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings = TextureToColorSettings(),
AlgoProgressCallback progress_callback = nullptr, AlgoCancelCallback cancel_callback = nullptr);
/**
* @brief Turn pre-computed per-face colors into a clustered color mesh (no texture/UV).
*
* Used for OBJ vertex colors and MTL face colors, which bypass texture sampling.
* Two routes are possible:
* - Low-poly meshes carrying per-vertex colors: the vertex colors are quantized
* into a small palette and the mesh is geometrically split along cluster
* boundaries, reproducing the split topology of the legacy OBJ vertex-color
* import. Output colors are then exact cluster centers, so mesh repair,
* re-clustering and smoothing are skipped.
* - Everything else: mesh repair, color clustering (K-Means or adaptive) and
* region smoothing, sharing the same pipeline as TextureToColor.
*
* @param[in] mesh Input triangle mesh
* @param[in] input_face_colors Pre-computed per-face RGB colors [0..255]
* @param[out] out_mesh Output mesh. Geometry is subdivided on the
* vertex-color route, and may still be replaced
* by mesh repair on the generic route.
* @param[out] out_face_colors Output per-face colors, one entry per out_mesh face
* @param[in] settings Algorithm parameters (target_colors_num, smooth_weight;
* oversampling_min_face_count doubles as the low-poly
* threshold for the vertex-color route)
* @param[in] progress_callback Progress callback
* @param[in] cancel_callback Cancel callback
* @param[in] vertex_colors Optional per-vertex RGBA [0..1]. Must match
* mesh.vertices in size to enable the vertex-color
* route; otherwise it is ignored.
* @return true on success, false on failure or cancellation
*/
bool ClusterAndSmooth(const TriMesh& mesh,
const std::vector<std::array<std::size_t, 3>>& input_face_colors,
TriMesh& out_mesh,
std::vector<std::array<std::size_t, 3>>& out_face_colors,
const TextureToColorSettings& settings = TextureToColorSettings(),
AlgoProgressCallback progress_callback = nullptr,
AlgoCancelCallback cancel_callback = nullptr,
const std::vector<std::array<float, 4>>& vertex_colors = {});
} // namespace tex2color
} // namespace Slic3r
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <admesh/stl.h>
#include "Point.hpp"
namespace Slic3r { namespace tex2color {
using TriVertex = stl_vertex;
using TriVertices = std::vector<stl_vertex>;
using TriFace = stl_triangle_vertex_indices;
using TriFaces = std::vector<stl_triangle_vertex_indices>;
struct TriMesh : ::indexed_triangle_set {
TriMesh() = default;
TriMesh(const TriMesh&) = default;
TriMesh& operator=(const TriMesh&) = default;
TriMesh(TriMesh&&) = default;
TriMesh& operator=(TriMesh&&) = default;
TriMesh(const ::indexed_triangle_set& d) : ::indexed_triangle_set(d) {}
TriMesh(::indexed_triangle_set&& d) : ::indexed_triangle_set(std::move(d)) {}
TriMesh(std::vector<stl_triangle_vertex_indices> indices_,
std::vector<stl_vertex> vertices_)
: ::indexed_triangle_set(std::move(indices_), std::move(vertices_)) {}
std::size_t facets_count() const { return indices.size(); }
};
} // namespace tex2color
} // namespace Slic3r
+52 -12
View File
@@ -1736,13 +1736,22 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
data.used_states[n] = true;
if (n >= 3) {
assert(n <= 16);
if (n <= 16) {
// Store "11" plus 4 bits of (n-3).
data.bitstream.insert(data.bitstream.end(), { true, true });
n -= 3;
assert(n <= int(EnforcerBlockerType::ExtruderMax));
// Store "11" plus 4 bits of (n-3), which covers states 3..17. State 18 and
// above set that nibble to 0b1111 and store (n-18) in a second nibble. This is
// the encoding the CONST_FILAMENTS table in Model.cpp already writes for
// colored mesh imports.
data.bitstream.insert(data.bitstream.end(), { true, true });
auto &bitstream = data.bitstream;
auto push_nibble = [&bitstream](int value) {
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx)
data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx));
bitstream.push_back(value & (uint64_t(0b0001) << bit_idx));
};
if (n <= 17) {
push_nibble(n - 3);
} else {
push_nibble(0b1111);
push_nibble(n - 18);
}
} else {
// Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams.
@@ -1810,6 +1819,12 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
n |= data.bitstream[ibit ++] << i;
return n;
};
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
// 3..17, or 0b1111 followed by a nibble of (state-18) above that.
auto decode_leaf_state = [&next_nibble]() {
const int nibble = next_nibble();
return EnforcerBlockerType(nibble == 0b1111 ? next_nibble() + 18 : nibble + 3);
};
parents.clear();
while (true) {
@@ -1818,8 +1833,8 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
int num_of_split_sides = code & 0b11;
int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1;
bool is_split = num_of_children != 0;
// Only valid if not is_split. Value of the second nibble was subtracted by 3, so it is added back.
auto state = is_split ? EnforcerBlockerType::NONE : EnforcerBlockerType((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2);
// Only valid if not is_split.
auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2));
// BBS
if (state == to_delete_filament)
@@ -1916,7 +1931,14 @@ void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bi
if (const bool is_split = (code & 0b11) != 0; is_split)
continue;
const uint8_t facet_state = (code & 0b1100) == 0b1100 ? read_next_nibble() + 3 : code >> 2;
uint8_t facet_state;
if ((code & 0b1100) == 0b1100) {
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
const uint8_t nibble = read_next_nibble();
facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3);
} else {
facet_state = code >> 2;
}
assert(facet_state < this->used_states.size());
if (facet_state >= this->used_states.size())
continue;
@@ -1946,9 +1968,13 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
auto num_children_or_state = [&next_nibble]() -> int {
int code = next_nibble();
int num_of_split_sides = code & 0b11;
return num_of_split_sides == 0 ?
((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) :
- num_of_split_sides - 1;
if (num_of_split_sides != 0)
return - num_of_split_sides - 1;
if ((code & 0b1100) != 0b1100)
return code >> 2;
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
const int nibble = next_nibble();
return nibble == 0b1111 ? next_nibble() + 18 : nibble + 3;
};
int state = num_children_or_state();
@@ -1983,6 +2009,20 @@ void TriangleSelector::seed_fill_unselect_all_triangles()
triangle.unselect_by_seed_fill();
}
void TriangleSelector::shift_states_above(EnforcerBlockerType threshold, int delta)
{
for (Triangle &triangle : m_triangles) {
if (triangle.is_split() || !triangle.valid())
continue;
EnforcerBlockerType s = triangle.get_state();
if (s >= threshold && s != EnforcerBlockerType::NONE) {
int new_val = (int)s + delta;
if (new_val >= 0)
triangle.set_state(EnforcerBlockerType(new_val));
}
}
}
void TriangleSelector::seed_fill_apply_on_triangles(EnforcerBlockerType new_state)
{
for (Triangle &triangle : m_triangles)
+23 -2
View File
@@ -17,7 +17,9 @@ enum class EnforcerBlockerType : int8_t {
BLOCKER = 2,
// For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN).
FUZZY_SKIN = ENFORCER,
// Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code.
// States 3..17 are serialized into 6 bits using a 2 bit prefix code; states 18 and above use
// one additional nibble (see TriangleSelector::serialize). ExtruderMax matches the last entry
// of CONST_FILAMENTS in Model.cpp, which encodes the same range for colored mesh imports.
Extruder1 = ENFORCER,
Extruder2 = BLOCKER,
Extruder3,
@@ -34,7 +36,23 @@ enum class EnforcerBlockerType : int8_t {
Extruder14,
Extruder15,
Extruder16,
ExtruderMax = Extruder16
Extruder17,
Extruder18,
Extruder19,
Extruder20,
Extruder21,
Extruder22,
Extruder23,
Extruder24,
Extruder25,
Extruder26,
Extruder27,
Extruder28,
Extruder29,
Extruder30,
Extruder31,
Extruder32,
ExtruderMax = Extruder32
};
// Type alias for the state mapping array to improve code readability
@@ -369,6 +387,9 @@ public:
// For all triangles, remove the flag indicating that the triangle was selected by seed fill.
void seed_fill_unselect_all_triangles();
// Shift all triangle states >= threshold by delta (used when inserting filaments)
void shift_states_above(EnforcerBlockerType threshold, int delta);
// For all triangles selected by seed fill, set new EnforcerBlockerType and remove flag indicating that triangle was selected by seed fill.
// The operation may merge split triangles if they are being assigned the same color.
void seed_fill_apply_on_triangles(EnforcerBlockerType new_state);
+5
View File
@@ -64,6 +64,11 @@ static constexpr double LARGE_BED_THRESHOLD = 2147;
// Orca: maximum number of extruders is 64. For SEMM printers, it defines maximum filament number.
static constexpr size_t MAXIMUM_EXTRUDER_NUMBER = 64;
// Orca: how many filament slots syncing an AMS setup may create. This was derived from
// EnforcerBlockerType::ExtruderMax, but that cap now covers 32 paintable filaments, so the AMS
// limit is pinned here to keep sync behaving as it does for projects without mixed-color filaments.
static constexpr size_t MAXIMUM_AMS_SYNC_FILAMENT_NUMBER = 16;
// Orca: maximum line width is 5 times the nozzle diameter
static constexpr float MAX_LINE_WIDTH_MULTIPLIER = 5;
+10
View File
@@ -355,6 +355,16 @@ set(SLIC3R_GUI_SOURCES
GUI/Monitor.hpp
GUI/MonitorPage.cpp
GUI/MonitorPage.hpp
GUI/MixedFilamentDialog.cpp
GUI/MixedFilamentDialog.hpp
GUI/GradientCurveEditor.cpp
GUI/GradientCurveEditor.hpp
GUI/ColorDecomposeDialog.cpp
GUI/ColorDecomposeDialog.hpp
GUI/ColorDecomposeSupport.cpp
GUI/ColorDecomposeSupport.hpp
GUI/TextureImportDialog.cpp
GUI/TextureImportDialog.hpp
GUI/Mouse3DController.cpp
GUI/Mouse3DController.hpp
GUI/MsgDialog.cpp
+12 -6
View File
@@ -682,13 +682,19 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
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;
// ORCA: extruder_id may be 0 (unset) or point past the colour list after a
// filament is deleted/remapped, so clamp the index instead of reading out of
// bounds.
if (!extruder_colors.empty()) {
int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1);
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]);
if (ban_light) {
new_color[3] = (255 - color_idx)/255.0f;
}
m.set_color(new_color);
// shader->set_uniform("uniform_color", new_color);
}
m.set_color(new_color);
// shader->set_uniform("uniform_color", new_color);
}
else {
if (idx <= extruder_colors.size()) {
+1 -1
View File
@@ -193,7 +193,7 @@ public:
void show_panels(CalibrationMethod method, const PrinterSeries printer_ser);
void on_device_connected(MachineObject* obj);
void on_device_connected(MachineObject* obj) override;
void update(MachineObject* obj) override;
@@ -48,8 +48,8 @@ public:
void create_page(wxWindow* parent);
void on_reset_page();
void on_device_connected(MachineObject* obj);
void on_reset_page() override;
void on_device_connected(MachineObject* obj) override;
void msw_rescale() override;
};
@@ -63,8 +63,8 @@ public:
long style = wxTAB_TRAVERSAL);
void create_page(wxWindow* parent);
void on_reset_page();
void on_device_connected(MachineObject* obj);
void on_reset_page() override;
void on_device_connected(MachineObject* obj) override;
void msw_rescale() override;
};
+951
View File
@@ -0,0 +1,951 @@
#include "ColorDecomposeDialog.hpp"
#include <algorithm>
#include <cmath>
#include <functional>
#include <memory>
#include <set>
#include <wx/sizer.h>
#include <wx/dcclient.h>
#include <wx/dcbuffer.h>
#include "wx/graphics.h"
#include "I18N.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "format.hpp"
#include "Widgets/ComboBox.hpp"
#include "Widgets/DropDown.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/Label.hpp"
#include "wxExtensions.hpp"
#include "ColorDecomposeSupport.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp"
namespace Slic3r {
namespace GUI {
static const wxColour COLOR_BRAND("#009688");
static const wxColour COLOR_BORDER_NORMAL("#EEEEEE");
static const wxColour COLOR_BG_CARD("#F8F8F8");
static const wxColour COLOR_LABEL_GREY("#ACACAC");
static const wxColour COLOR_TEXT_DARK("#262E30");
static const wxColour COLOR_DIVIDER("#EEEEEE");
// Standard CMYW base colors
static const wxColour CMYW_CYAN(0, 255, 255);
static const wxColour CMYW_MAGENTA(255, 0, 255);
static const wxColour CMYW_YELLOW(255, 255, 0);
static const wxColour CMYW_WHITE(255, 255, 255);
// Standard RYBW base colors
static const wxColour RYBW_RED(255, 0, 0);
static const wxColour RYBW_YELLOW(255, 255, 0);
static const wxColour RYBW_BLUE(0, 0, 255);
static const wxColour RYBW_WHITE(255, 255, 255);
static size_t mode_index(DecomposeMode mode)
{
return static_cast<size_t>(mode);
}
static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color)
{
return {
static_cast<unsigned char>(color.Red()),
static_cast<unsigned char>(color.Green()),
static_cast<unsigned char>(color.Blue())
};
}
static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback)
{
wxColour color(hex);
return color.IsOk() ? color : fallback;
}
static bool same_rgb(const wxColour& lhs, const wxColour& rhs)
{
return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue();
}
static DecomposeBaseColor standard_base_color_from_key(const std::string& key)
{
if (key == "Cyan") return DecomposeBaseColor::Cyan;
if (key == "Magenta") return DecomposeBaseColor::Magenta;
if (key == "Yellow") return DecomposeBaseColor::Yellow;
if (key == "White") return DecomposeBaseColor::White;
if (key == "Red") return DecomposeBaseColor::Red;
if (key == "Green") return DecomposeBaseColor::Green;
if (key == "Blue") return DecomposeBaseColor::Blue;
return DecomposeBaseColor::None;
}
static wxColour pure_color_for_base(DecomposeBaseColor base)
{
switch (base) {
case DecomposeBaseColor::Cyan: return CMYW_CYAN;
case DecomposeBaseColor::Magenta: return CMYW_MAGENTA;
case DecomposeBaseColor::Yellow: return CMYW_YELLOW;
case DecomposeBaseColor::White: return CMYW_WHITE;
case DecomposeBaseColor::Red: return RYBW_RED;
case DecomposeBaseColor::Blue: return RYBW_BLUE;
default: return *wxBLACK;
}
}
static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color)
{
if (mode == DecomposeMode::CMYW) {
if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan;
if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta;
if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow;
if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White;
} else if (mode == DecomposeMode::RYBW) {
if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red;
if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow;
if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue;
if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White;
}
return DecomposeBaseColor::None;
}
static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe,
const wxColour& fallback)
{
ColorDecomposeResult result;
result.mode = recipe.mode;
result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback);
for (const auto& comp_recipe : recipe.components) {
DecomposeComponent comp;
comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback);
comp.ratio = comp_recipe.ratio;
comp.filament_index = static_cast<int>(comp_recipe.filament_index);
comp.base_color = standard_base_color_from_key(comp_recipe.base_color);
if (comp.base_color == DecomposeBaseColor::None)
comp.base_color = standard_base_color_for(recipe.mode, comp.colour);
result.components.push_back(comp);
}
return result;
}
static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1)
{
const int h = parent->FromDIP(1);
int w = fixed_width > 0 ? fixed_width : -1;
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h));
panel->SetMinSize(wxSize(w, h));
if (fixed_width > 0)
panel->SetMaxSize(wxSize(fixed_width, h));
panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER));
return panel;
}
static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text)
{
auto* label = new wxStaticText(parent, wxID_ANY, text);
label->SetFont(Label::Body_11);
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY));
return label;
}
static void match_parent_bg(wxWindow* w, const wxColour& bg)
{
w->SetBackgroundColour(bg);
}
static bool material_type_matches(const std::string& a, const std::string& b)
{
if (a.empty() || b.empty())
return false;
return a == b || a == b + " Basic" || b == a + " Basic";
}
ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent,
int filament_idx,
const wxColour& target_color,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& filament_names,
const std::vector<std::string>& filament_types,
size_t current_filament_count,
size_t max_filament_count,
std::vector<size_t> physical_config_indices)
: DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition,
wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_filament_idx(filament_idx)
, m_target_color(target_color)
, m_physical_colors(physical_colors)
, m_filament_names(filament_names)
, m_filament_types(filament_types)
, m_current_filament_count(current_filament_count)
, m_max_filament_count(max_filament_count)
, m_physical_config_indices(std::move(physical_config_indices))
{
for (const auto& t : m_filament_types) {
if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end())
m_project_types.push_back(t);
}
if (m_filament_idx >= 0 && static_cast<size_t>(m_filament_idx) < m_filament_types.size())
m_preferred_type = m_filament_types[m_filament_idx];
else if (!m_project_types.empty())
m_preferred_type = m_project_types.front();
build_ui();
wxGetApp().UpdateDlgDarkUI(this);
// Restore target swatch after dark mode color remapping
if (m_target_swatch) {
m_target_swatch->SetBackgroundColour(m_target_color);
m_target_swatch->Refresh();
}
update_card_visibility();
Fit();
compute_decomposition();
update_matched_color_display();
update_ok_button_state();
}
void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect)
{
(void)suggested_rect;
Fit();
Refresh();
}
void ColorDecomposeDialog::build_ui()
{
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
const int selector_side_margin = FromDIP(26);
const int selector_top_gap = FromDIP(22);
const int content_side_margin = FromDIP(30);
const int target_section_top_gap = FromDIP(18);
main_sizer->AddSpacer(selector_top_gap);
main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin);
main_sizer->AddSpacer(target_section_top_gap);
main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
main_sizer->AddSpacer(FromDIP(16));
main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
main_sizer->AddSpacer(FromDIP(16));
main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
main_sizer->AddSpacer(FromDIP(16));
main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin);
SetSizer(main_sizer);
SetMinSize(wxSize(FromDIP(477), FromDIP(380)));
Fit();
CenterOnParent();
}
wxBoxSizer* ColorDecomposeDialog::create_filament_selector()
{
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY);
m_type_combo->SetFont(Label::Body_13);
m_combo_item_types.clear();
int default_sel = -1;
// --- Group 1: Project filament list (deduplicated by type) ---
m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
m_combo_item_types.push_back(std::string());
std::set<std::string> seen_types;
for (size_t i = 0; i < m_filament_names.size(); ++i) {
const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA";
if (!seen_types.insert(type).second)
continue;
int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i]));
m_combo_item_types.push_back(type);
if (type == m_preferred_type && default_sel < 0)
default_sel = idx;
}
// --- Group 2: Standard mode material recommendations ---
static const char* kStandardTypes[] = {
kDecomposePlaBasicType
};
m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
m_combo_item_types.push_back(std::string());
for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) {
// Always show standard recommendations, even if the same type already
// appears in the project filament list above.
const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s];
int idx = m_type_combo->Append(wxString::FromUTF8(label));
m_combo_item_types.push_back(kStandardTypes[s]);
if (kStandardTypes[s] == m_preferred_type && default_sel < 0)
default_sel = idx;
}
if (default_sel < 0) {
for (int i = 0; i < static_cast<int>(m_combo_item_types.size()); ++i) {
if (!m_combo_item_types[i].empty()) {
default_sel = i;
break;
}
}
}
if (default_sel >= 0) {
m_type_combo->SetSelection(default_sel);
if (!m_combo_item_types[default_sel].empty())
m_preferred_type = m_combo_item_types[default_sel];
}
m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) {
evt.StopPropagation();
int sel = m_type_combo->GetSelection();
if (sel >= 0 && static_cast<size_t>(sel) < m_combo_item_types.size()
&& !m_combo_item_types[sel].empty()) {
m_preferred_type = m_combo_item_types[sel];
}
update_card_visibility();
compute_decomposition();
update_matched_color_display();
update_ok_button_state();
});
sizer->Add(m_type_combo, 1, wxEXPAND);
return sizer;
}
static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size)
{
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size));
panel->SetBackgroundColour(color);
panel->SetMinSize(wxSize(size, size));
panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
panel->Bind(wxEVT_PAINT, [panel](wxPaintEvent&) {
wxAutoBufferedPaintDC dc(panel);
wxSize sz = panel->GetClientSize();
wxColour c = panel->GetBackgroundColour();
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(c));
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
// Mirror sidebar (FilamentBitmapUtils::create_single_filament_bitmap):
// gray border for near-white in light mode so white swatches stay
// visible on a white background; light border for near-black in dark mode.
const bool light_mode = !wxGetApp().dark_mode();
if ((light_mode && c.Red() > 224 && c.Green() > 224 && c.Blue() > 224) ||
(!light_mode && c.Red() < 45 && c.Green() < 45 && c.Blue() < 45)) {
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.SetPen(wxPen(light_mode ? wxColour(130, 130, 128) : wxColour(207, 207, 207),
1, wxPENSTYLE_SOLID));
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
}
});
return panel;
}
wxBoxSizer* ColorDecomposeDialog::create_target_color_section()
{
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color"));
label->SetFont(Label::Head_14);
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19));
m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
m_target_rgb_text = new wxStaticText(this, wxID_ANY,
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
m_target_rgb_text->SetFont(Label::Body_13);
m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
m_matched_rgb_text = new wxStaticText(this, wxID_ANY,
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
m_matched_rgb_text->SetFont(Label::Head_13);
m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL);
return sizer;
}
wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode,
const wxString& title)
{
const int pad = FromDIP(12);
auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
card->SetBackgroundStyle(wxBG_STYLE_PAINT);
auto* card_sizer = new wxBoxSizer(wxVERTICAL);
auto* title_sizer = new wxBoxSizer(wxHORIZONTAL);
auto* title_label = new wxStaticText(card, wxID_ANY, title);
title_label->SetFont(Label::Body_14);
title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL);
auto* chk = new ::CheckBox(card);
chk->SetValue(mode == m_selected_mode);
match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD));
switch (mode) {
case DecomposeMode::MaterialList: m_chk_material_list = chk; break;
case DecomposeMode::CMYW: m_chk_cmyw = chk; break;
case DecomposeMode::RYBW: m_chk_rybw = chk; break;
}
chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) {
select_mode(mode);
e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue()
});
title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL);
card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad);
card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8));
auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL);
card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
auto& controls = m_mode_cards[mode_index(mode)];
controls.card = card;
controls.components_sizer = colors_sizer;
card->SetSizer(card_sizer);
card->SetMinSize(wxSize(FromDIP(128), FromDIP(111)));
card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111)));
card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) {
wxBufferedPaintDC dc(card);
wxSize sz = card->GetClientSize();
dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE)));
dc.Clear();
bool selected = (m_selected_mode == mode);
wxColour border_col = selected
? StateColor::darkModeColorFor(COLOR_BRAND)
: StateColor::darkModeColorFor(COLOR_BORDER_NORMAL);
const int border_width = FromDIP(selected ? 2 : 1);
const double inset = border_width / 2.0;
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc) {
gc->SetPen(wxPen(border_col, border_width));
gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8));
} else {
const int fallback_inset = (border_width + 1) / 2;
dc.SetPen(wxPen(border_col, border_width));
dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8));
}
});
std::function<void(wxWindow*)> bind_click;
bind_click = [this, mode, chk, &bind_click](wxWindow* w) {
if (w == chk || dynamic_cast<::CheckBox*>(w))
return;
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
select_mode(mode);
});
w->SetCursor(wxCursor(wxCURSOR_HAND));
for (auto* child : w->GetChildren())
bind_click(child);
};
bind_click(card);
return card;
}
wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section()
{
auto* sizer = new wxBoxSizer(wxVERTICAL);
auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition"));
section_label->SetFont(Label::Head_14);
section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4));
auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL);
// --- Arbitrary mode column (wrapped in a panel so the whole column hides together) ---
m_arb_column_panel = new wxPanel(this, wxID_ANY);
m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
auto* arb_col = new wxBoxSizer(wxVERTICAL);
{
auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL);
arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL);
arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList,
_L("Material List"));
arb_col->Add(m_card_material_list, 0, wxEXPAND);
}
m_arb_column_panel->SetSizer(arb_col);
modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16));
// --- Standard mode column ---
auto* std_col = new wxBoxSizer(wxVERTICAL);
{
auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL);
std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL);
std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL);
m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW");
cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12));
m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW");
cards_sizer->Add(m_card_rybw, 0);
std_col->Add(cards_sizer, 0, wxEXPAND);
}
modes_sizer->Add(std_col, 0, wxEXPAND);
sizer->Add(modes_sizer, 0, wxEXPAND);
m_no_card_hint = new wxStaticText(this, wxID_ANY,
_L("At least two filaments of the same material type are required for decomposition"));
m_no_card_hint->SetFont(Label::Body_13);
m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
m_no_card_hint->Wrap(FromDIP(400));
m_no_card_hint->Hide();
sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8));
m_limit_warning_panel = new wxPanel(this, wxID_ANY);
m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL);
auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY,
create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16),
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString);
m_limit_warning_text->SetFont(Label::Body_13);
m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B")));
m_limit_warning_text->Wrap(FromDIP(400));
warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6));
warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND);
m_limit_warning_panel->SetSizer(warning_sizer);
m_limit_warning_panel->Hide();
sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8));
return sizer;
}
wxBoxSizer* ColorDecomposeDialog::create_button_panel()
{
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->AddStretchSpacer();
m_btn_cancel = new Button(this, _L("Cancel"));
m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
m_btn_ok = new Button(this, _L("OK"));
m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
EndModal(wxID_OK);
});
sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12));
sizer->Add(m_btn_ok, 0);
return sizer;
}
void ColorDecomposeDialog::select_mode(DecomposeMode mode)
{
m_selected_mode = mode;
m_result = m_mode_results[mode_index(mode)];
update_card_styles();
update_matched_color_display();
update_ok_button_state();
}
void ColorDecomposeDialog::update_card_styles()
{
if (m_card_material_list) m_card_material_list->Refresh();
if (m_card_cmyw) m_card_cmyw->Refresh();
if (m_card_rybw) m_card_rybw->Refresh();
if (m_chk_material_list)
m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList);
if (m_chk_cmyw)
m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW);
if (m_chk_rybw)
m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW);
}
void ColorDecomposeDialog::update_card_visibility()
{
// Count physical filaments of the same type (excluding the source filament)
int same_type_count = 0;
for (size_t i = 0; i < m_filament_types.size(); ++i) {
if (static_cast<int>(i) == m_filament_idx)
continue;
if (material_type_matches(m_filament_types[i], m_preferred_type))
++same_type_count;
}
bool show_arb = (same_type_count >= 2);
bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType);
bool show_rybw = (m_preferred_type == kDecomposePlaBasicType);
if (m_arb_column_panel) m_arb_column_panel->Show(show_arb);
if (m_card_material_list) m_card_material_list->Show(show_arb);
if (m_card_cmyw) m_card_cmyw->Show(show_cmyw);
if (m_card_rybw) m_card_rybw->Show(show_rybw);
bool any_visible = show_arb || show_cmyw || show_rybw;
if (m_no_card_hint)
m_no_card_hint->Show(!any_visible);
// Auto-select a visible mode when current selection becomes hidden
if (any_visible) {
bool cur_visible = false;
if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true;
if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true;
if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true;
if (!cur_visible) {
if (show_arb) select_mode(DecomposeMode::MaterialList);
else if (show_cmyw) select_mode(DecomposeMode::CMYW);
else select_mode(DecomposeMode::RYBW);
}
}
Layout();
update_ok_button_state();
}
void ColorDecomposeDialog::update_filament_limit_warning()
{
if (!m_limit_warning_panel || !m_limit_warning_text)
return;
size_t missing_new = 0;
if (m_missing_calculator) {
missing_new = m_missing_calculator(m_result);
} else {
const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast<size_t>(m_filament_idx) : size_t(-1);
const std::vector<size_t>* indices =
m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices;
missing_new = count_decompose_new_physical_filaments(
m_result, m_physical_colors, m_filament_types, source_physical_idx, indices);
}
// A result with fewer than 2 components (e.g. target color is already a
// standard base color shown as "100%") creates no mixed filament and no new
// physical filament, so it can never exceed the limit.
const bool creates_mixed = m_result.components.size() >= 2;
// +1 for the mixed filament slot that will be created after decomposition.
const size_t needed = m_current_filament_count + missing_new + 1;
const bool blocked = creates_mixed && needed > m_max_filament_count;
const bool was_shown = m_limit_warning_panel->IsShown();
if (!blocked) {
if (was_shown) {
m_limit_warning_panel->Hide();
Layout();
Fit();
}
return;
}
wxString mode_name;
switch (m_selected_mode) {
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
}
const wxString warning_text = format_wxstr(
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
m_max_filament_count, mode_name);
// Show first so the panel is laid out and the text control gets its real
// width, then wrap to that width so the paragraph fills the content area.
m_limit_warning_panel->Show();
Layout();
const int avail = m_limit_warning_text->GetClientSize().x;
m_limit_warning_text->SetLabel(warning_text);
if (avail > FromDIP(50))
m_limit_warning_text->Wrap(avail);
Layout();
// Only resize when the warning panel actually toggled from hidden to shown.
// While already visible, switching modes must not re-Fit the dialog, which
// would make it jump on every card switch. Fit keeps the user-moved position.
if (!was_shown) {
Fit();
}
}
void ColorDecomposeDialog::set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn)
{
m_missing_calculator = std::move(fn);
update_ok_button_state();
}
void ColorDecomposeDialog::update_ok_button_state()
{
if (!m_btn_ok) return;
update_filament_limit_warning();
bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown())
|| (m_card_cmyw && m_card_cmyw->IsShown())
|| (m_card_rybw && m_card_rybw->IsShown());
const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown();
m_btn_ok->Enable(any_card_visible && !blocked);
Layout();
}
void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode)
{
auto& controls = m_mode_cards[mode_index(mode)];
auto* sizer = controls.components_sizer;
auto* card = controls.card;
if (!sizer || !card)
return;
sizer->Clear(true);
const auto& components = m_mode_results[mode_index(mode)].components;
const size_t count = components.size();
if (count == 0) {
card->Layout();
card->Refresh();
return;
}
const int swatch_sz = FromDIP(24);
const int plus_gap = FromDIP(24);
const wxFont& ratio_font = Label::Body_13;
auto bind_select = [this, mode](wxWindow* w) {
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
select_mode(mode);
});
w->SetCursor(wxCursor(wxCURSOR_HAND));
};
for (size_t i = 0; i < count; ++i) {
auto* col = new wxBoxSizer(wxVERTICAL);
auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz);
bind_select(swatch);
col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL);
auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio));
ratio_text->SetFont(ratio_font);
ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD));
bind_select(ratio_text);
col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4));
sizer->Add(col, 0, wxALIGN_TOP);
if (i + 1 < count) {
sizer->AddStretchSpacer();
auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz));
plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz));
plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz));
plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD));
auto* plus_sizer = new wxBoxSizer(wxVERTICAL);
auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+");
plus_label->SetFont(Label::Body_13);
plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
bind_select(plus_panel);
bind_select(plus_label);
plus_sizer->AddStretchSpacer();
plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL);
plus_sizer->AddStretchSpacer();
plus_panel->SetSizer(plus_sizer);
sizer->Add(plus_panel, 0, wxALIGN_TOP);
sizer->AddStretchSpacer();
}
}
const int card_width = FromDIP(128 + (count > 2 ? static_cast<int>(count - 2) * 31 : 0));
card->SetMinSize(wxSize(card_width, FromDIP(111)));
card->SetMaxSize(wxSize(card_width, FromDIP(111)));
card->Layout();
card->Refresh();
}
void ColorDecomposeDialog::update_mode_card_contents()
{
update_mode_card_content(DecomposeMode::MaterialList);
update_mode_card_content(DecomposeMode::CMYW);
update_mode_card_content(DecomposeMode::RYBW);
Layout();
Fit();
}
void ColorDecomposeDialog::update_matched_color_display()
{
if (!m_result.matched_color.IsOk())
m_result.matched_color = m_target_color;
if (m_matched_swatch) {
m_matched_swatch->SetBackgroundColour(m_result.matched_color);
m_matched_swatch->Refresh();
}
if (m_matched_rgb_text) {
m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d",
m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue()));
}
}
bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const
{
// Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic.
if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) {
if (m_preferred_type != kDecomposePlaBasicType)
return false;
} else {
return false;
}
static const DecomposeBaseColor cmyw_bases[] = {
DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta,
DecomposeBaseColor::Yellow, DecomposeBaseColor::White
};
static const DecomposeBaseColor rybw_bases[] = {
DecomposeBaseColor::Red, DecomposeBaseColor::Yellow,
DecomposeBaseColor::Blue, DecomposeBaseColor::White
};
const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases;
const size_t base_count = (mode == DecomposeMode::CMYW)
? sizeof(cmyw_bases) / sizeof(cmyw_bases[0])
: sizeof(rybw_bases) / sizeof(rybw_bases[0]);
const std::string target_hex = decompose_normalize_color_hex(
m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
for (size_t i = 0; i < base_count; ++i) {
const DecomposeBaseColor base = bases[i];
DecomposeOfficialComponent official =
lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base));
if (decompose_normalize_color_hex(official.color_hex) != target_hex)
continue;
out = ColorDecomposeResult{};
out.mode = mode;
out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color);
DecomposeComponent comp;
comp.colour = out.matched_color;
comp.ratio = 100;
comp.filament_index = -1;
comp.base_color = base;
out.components.push_back(comp);
return true;
}
return false;
}
void ColorDecomposeDialog::compute_decomposition()
{
auto fallback_result = [this](DecomposeMode mode, const std::vector<DecomposeComponent>& components) {
ColorDecomposeResult result;
result.mode = mode;
result.components = components;
int total = 0;
double r = 0.0, g = 0.0, b = 0.0;
for (const auto& comp : result.components)
total += comp.ratio;
if (total <= 0)
total = 100;
for (const auto& comp : result.components) {
const double w = static_cast<double>(comp.ratio) / total;
r += comp.colour.Red() * w;
g += comp.colour.Green() * w;
b += comp.colour.Blue() * w;
}
result.matched_color = result.components.empty()
? m_target_color
: wxColour(static_cast<unsigned char>(std::clamp(r, 0.0, 255.0)),
static_cast<unsigned char>(std::clamp(g, 0.0, 255.0)),
static_cast<unsigned char>(std::clamp(b, 0.0, 255.0)));
return result;
};
std::vector<ColorDecomposePhysicalFilament> physical_filaments;
physical_filaments.reserve(m_physical_colors.size());
for (size_t i = 0; i < m_physical_colors.size(); ++i) {
if (m_filament_idx >= 0 && i == static_cast<size_t>(m_filament_idx))
continue;
ColorDecomposePhysicalFilament filament;
filament.color_hex = m_physical_colors[i];
filament.name = i < m_filament_names.size() ? m_filament_names[i] : "";
filament.type = i < m_filament_types.size() ? m_filament_types[i] : "";
filament.filament_index = static_cast<unsigned int>(i + 1);
physical_filaments.push_back(std::move(filament));
}
const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color);
auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type);
if (material_recipe.valid) {
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
to_dialog_result(material_recipe, m_target_color);
} else {
std::vector<DecomposeComponent> components;
for (size_t i = 0; i < std::min<size_t>(2, physical_filaments.size()); ++i) {
DecomposeComponent comp;
comp.colour = wxColour(physical_filaments[i].color_hex);
comp.ratio = 50;
comp.filament_index = static_cast<int>(physical_filaments[i].filament_index);
components.push_back(comp);
}
if (components.empty()) {
components.push_back({m_target_color, 100, -1});
} else if (components.size() == 1) {
components.front().ratio = 100;
}
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
fallback_result(DecomposeMode::MaterialList, components);
}
ColorDecomposeResult single_base;
if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) {
m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base;
} else {
auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type);
m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid
? to_dialog_result(cmyw_recipe, m_target_color)
: fallback_result(DecomposeMode::CMYW, {
{CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
{CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan}
});
}
if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) {
m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base;
} else {
auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type);
m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid
? to_dialog_result(rybw_recipe, m_target_color)
: fallback_result(DecomposeMode::RYBW, {
{RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
{RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue}
});
}
m_result = m_mode_results[mode_index(m_selected_mode)];
update_mode_card_contents();
update_ok_button_state();
}
} // namespace GUI
} // namespace Slic3r
+152
View File
@@ -0,0 +1,152 @@
#ifndef slic3r_ColorDecomposeDialog_hpp_
#define slic3r_ColorDecomposeDialog_hpp_
#include <array>
#include <functional>
#include <string>
#include <vector>
#include <utility>
#include <wx/colour.h>
#include <wx/panel.h>
#include <wx/statbmp.h>
#include <wx/stattext.h>
#include "GUI_Utils.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp"
class Button;
class CheckBox;
class ComboBox;
namespace Slic3r {
namespace GUI {
using DecomposeMode = ColorDecomposeRecipeMode;
enum class DecomposeBaseColor {
None,
Cyan,
Magenta,
Yellow,
White,
Red,
Green,
Blue
};
struct DecomposeComponent {
wxColour colour;
int ratio{50}; // percentage
int filament_index{-1}; // 1-based physical filament index, -1 if standard base color
DecomposeBaseColor base_color{DecomposeBaseColor::None};
};
struct ColorDecomposeResult {
DecomposeMode mode{DecomposeMode::MaterialList};
wxColour matched_color;
std::vector<DecomposeComponent> components;
};
class ColorDecomposeDialog : public DPIDialog
{
public:
ColorDecomposeDialog(wxWindow* parent,
int filament_idx,
const wxColour& target_color,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& filament_names,
const std::vector<std::string>& filament_types,
size_t current_filament_count = 0,
size_t max_filament_count = 32,
std::vector<size_t> physical_config_indices = {});
ColorDecomposeResult get_result() const { return m_result; }
// Override the "new physical filaments" count used by the filament-limit
// warning. The Texture import path supplies its own calculator so the
// pre-check shares the exact reuse rule as its write-back (existing +
// virtual physical filaments), instead of the project-config based default
// that cannot see not-yet-committed virtual base colors.
void set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn);
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void build_ui();
wxBoxSizer* create_filament_selector();
wxBoxSizer* create_target_color_section();
wxBoxSizer* create_mode_selection_section();
wxPanel* create_mode_card(wxWindow* parent, DecomposeMode mode, const wxString& title);
wxBoxSizer* create_button_panel();
void select_mode(DecomposeMode mode);
void update_card_styles();
void update_card_visibility();
void update_mode_card_content(DecomposeMode mode);
void update_mode_card_contents();
void update_matched_color_display();
void update_ok_button_state();
void update_filament_limit_warning();
void compute_decomposition();
// When the target color is exactly one of the standard base colors for the
// preferred type, the standard card should show that base at 100% instead of
// a mix. PLA Basic covers CMYW and RYBW.
bool try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const;
struct ModeCardControls {
wxPanel* card{nullptr};
wxBoxSizer* components_sizer{nullptr};
};
ColorDecomposeResult m_result;
std::array<ColorDecomposeResult, 3> m_mode_results;
std::array<ModeCardControls, 3> m_mode_cards;
int m_filament_idx{-1};
wxColour m_target_color;
std::vector<std::string> m_physical_colors;
std::vector<std::string> m_filament_names;
std::vector<std::string> m_filament_types;
std::vector<std::string> m_project_types;
std::string m_preferred_type;
// Dropdown selectable item index -> material type string
std::vector<std::string> m_combo_item_types;
size_t m_current_filament_count{0};
size_t m_max_filament_count{32};
std::vector<size_t> m_physical_config_indices;
std::function<size_t(const ColorDecomposeResult&)> m_missing_calculator;
// UI controls
ComboBox* m_type_combo{nullptr};
wxPanel* m_target_swatch{nullptr};
wxStaticText* m_target_rgb_text{nullptr};
wxPanel* m_matched_swatch{nullptr};
wxStaticText* m_matched_rgb_text{nullptr};
// Mode cards
wxPanel* m_card_material_list{nullptr};
wxPanel* m_card_cmyw{nullptr};
wxPanel* m_card_rybw{nullptr};
wxPanel* m_arb_column_panel{nullptr};
CheckBox* m_chk_material_list{nullptr};
CheckBox* m_chk_cmyw{nullptr};
CheckBox* m_chk_rybw{nullptr};
DecomposeMode m_selected_mode{DecomposeMode::MaterialList};
// Hint shown when no mode card is visible
wxStaticText* m_no_card_hint{nullptr};
// Warning shown when decomposition would exceed filament limit
wxPanel* m_limit_warning_panel{nullptr};
wxStaticText* m_limit_warning_text{nullptr};
Button* m_btn_ok{nullptr};
Button* m_btn_cancel{nullptr};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_ColorDecomposeDialog_hpp_
+386
View File
@@ -0,0 +1,386 @@
#include "ColorDecomposeSupport.hpp"
#include "MixedFilamentDialog.hpp"
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
#include "I18N.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Utils.hpp"
#include "nlohmann/json.hpp"
#include <fstream>
#include <algorithm>
#include <cctype>
using json = nlohmann::json;
namespace Slic3r { namespace GUI {
std::string decompose_normalize_color_hex(std::string color)
{
if (color.size() >= 7)
color = color.substr(0, 7);
std::transform(color.begin(), color.end(), color.begin(), [](unsigned char c) {
return static_cast<char>(std::toupper(c));
});
return color;
}
const char* decompose_base_color_en(DecomposeBaseColor color)
{
switch (color) {
case DecomposeBaseColor::Cyan: return "Cyan";
case DecomposeBaseColor::Magenta: return "Magenta";
case DecomposeBaseColor::Yellow: return "Yellow";
case DecomposeBaseColor::White: return "White";
case DecomposeBaseColor::Red: return "Red";
case DecomposeBaseColor::Green: return "Green";
case DecomposeBaseColor::Blue: return "Blue";
default: return "";
}
}
wxString decompose_base_color_display(DecomposeBaseColor color)
{
switch (color) {
case DecomposeBaseColor::Cyan: return _L("Cyan");
case DecomposeBaseColor::Magenta: return _L("Magenta");
case DecomposeBaseColor::Yellow: return _L("Yellow");
case DecomposeBaseColor::White: return _L("White");
case DecomposeBaseColor::Red: return _L("Red");
case DecomposeBaseColor::Green: return _L("Green");
case DecomposeBaseColor::Blue: return _L("Blue");
default: return wxString();
}
}
std::string decompose_basic_type_from_source(size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_types)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
if (source_config_idx < filament_id_opt->values.size()) {
const std::string& filament_id = filament_id_opt->values[source_config_idx];
if (filament_id == kDecomposePetgFilamentId)
return kDecomposePetgBasicType;
if (filament_id == kDecomposePlaFilamentId)
return kDecomposePlaBasicType;
}
}
if (source_physical_idx < physical_types.size()) {
const std::string& type = physical_types[source_physical_idx];
if (type == kDecomposePetgShortType || type == kDecomposePetgBasicType)
return kDecomposePetgBasicType;
if (type == kDecomposePlaShortType || type == kDecomposePlaBasicType)
return kDecomposePlaBasicType;
}
return kDecomposePlaBasicType;
}
std::string decompose_basic_filament_id(const std::string& basic_type)
{
if (basic_type == kDecomposePetgBasicType)
return kDecomposePetgFilamentId;
return kDecomposePlaFilamentId;
}
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
if (!component.filament_id.empty()) {
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
while (filament_id_opt->values.size() <= config_idx)
filament_id_opt->values.push_back("");
filament_id_opt->values[config_idx] = component.filament_id;
}
}
const std::string type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType :
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : "";
if (!type.empty()) {
if (auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type")) {
while (type_opt->values.size() <= config_idx)
type_opt->values.push_back("");
type_opt->values[config_idx] = type;
}
}
}
DecomposeOfficialComponent lookup_decompose_official_component(
const std::string& basic_type,
DecomposeBaseColor base_color,
const wxColour& fallback)
{
DecomposeOfficialComponent result;
result.base_color = base_color;
result.color_hex = decompose_normalize_color_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
result.filament_id = decompose_basic_filament_id(basic_type);
const char* color_name = decompose_base_color_en(base_color);
if (color_name[0] == '\0')
return result;
// Some materials name a standard base color differently in the color-code
// table. PETG Basic's RYBW blue base is "Reflex Blue" (deep blue, B00,
// #001489), not "Blue". Match by an ordered list of exact English names so
// "Navy Blue" (B01, #0086D6) is never picked up by mistake.
std::vector<std::string> candidate_names;
candidate_names.emplace_back(color_name);
if (base_color == DecomposeBaseColor::Blue && basic_type == kDecomposePetgBasicType)
candidate_names.emplace_back("Reflex Blue");
std::ifstream ifs(resources_dir() + "/profiles/BBL/filament/filaments_color_codes.json");
if (!ifs)
return result;
json root = json::parse(ifs, nullptr, false);
if (root.is_discarded() || !root.contains("data") || !root["data"].is_array())
return result;
for (const std::string& candidate : candidate_names) {
for (const auto& item : root["data"]) {
if (!item.is_object() || item.value("fila_type", "") != basic_type)
continue;
if (!item.contains("fila_color_name"))
continue;
const auto& names = item["fila_color_name"];
if (!names.is_object() || names.value("en", "") != candidate)
continue;
if (item.contains("fila_color") && item["fila_color"].is_array() && !item["fila_color"].empty())
result.color_hex = decompose_normalize_color_hex(item["fila_color"][0].get<std::string>());
result.filament_id = item.value("fila_id", result.filament_id);
return result;
}
}
return result;
}
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type)
{
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
if (source_config_idx < preset_bundle.filament_presets.size()) {
const std::string& source_name = preset_bundle.filament_presets[source_config_idx];
if (source_name.find(std::string(kDecomposeBambuPresetPrefix) + basic_type) != std::string::npos)
return source_name;
}
const std::string prefix = std::string(kDecomposeBambuPresetPrefix) + basic_type + " @BBL ";
for (const std::string& preset_name : preset_bundle.filament_presets) {
if (preset_name.find(prefix) == 0)
return preset_name;
}
return {};
}
std::string official_basic_type_from_preset_name(const std::string& preset_name)
{
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePlaBasicType) != std::string::npos)
return kDecomposePlaBasicType;
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePetgBasicType) != std::string::npos)
return kDecomposePetgBasicType;
return {};
}
std::string filament_type_for_color_decompose(Preset* preset)
{
if (!preset)
return kDecomposePlaShortType;
std::string display_type;
std::string ft = preset->config.get_filament_type(display_type);
const std::string basic = official_basic_type_from_preset_name(preset->name);
if (!basic.empty())
ft = basic;
if (ft.empty())
ft = kDecomposePlaShortType;
return ft;
}
int find_existing_decompose_component(
const DecomposeOfficialComponent& component,
const std::vector<std::string>& physical_colors,
const std::vector<size_t>& physical_config_indices,
size_t source_config_idx)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id");
auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type");
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
const size_t num_physical = physical_colors.size();
const std::string expected_basic_type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType :
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : "";
const std::string expected_short_type = expected_basic_type == kDecomposePetgBasicType ? kDecomposePetgShortType :
expected_basic_type == kDecomposePlaBasicType ? kDecomposePlaShortType : "";
const std::string expected_preset_part = expected_basic_type.empty() ? "" : std::string(kDecomposeBambuPresetPrefix) + expected_basic_type;
for (size_t i = 0; i < num_physical && i < physical_config_indices.size(); ++i) {
const size_t config_idx = physical_config_indices[i];
const std::string slot_color = decompose_normalize_color_hex(physical_colors[i]);
const std::string slot_filament_id = (filament_id_opt && config_idx < filament_id_opt->values.size()) ? filament_id_opt->values[config_idx] : "";
const std::string slot_type = (type_opt && config_idx < type_opt->values.size()) ? type_opt->values[config_idx] : "";
const std::string preset_name = config_idx < preset_bundle.filament_presets.size() ? preset_bundle.filament_presets[config_idx] : "";
if (config_idx == source_config_idx) {
continue;
}
if (slot_color != component.color_hex) {
continue;
}
if (!component.filament_id.empty() && slot_filament_id == component.filament_id) {
return static_cast<int>(config_idx + 1);
}
if (!expected_basic_type.empty() && (slot_type == expected_basic_type || slot_type == expected_short_type)) {
return static_cast<int>(config_idx + 1);
}
if (!expected_preset_part.empty() && preset_name.find(expected_preset_part) != std::string::npos) {
return static_cast<int>(config_idx + 1);
}
const bool has_material_hint = !slot_filament_id.empty() || !slot_type.empty() || !preset_name.empty();
if (!expected_basic_type.empty() && has_material_hint)
continue;
return static_cast<int>(config_idx + 1);
}
return -1;
}
bool prepare_decompose_mixed_result(
const ColorDecomposeResult& result,
size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
const std::vector<size_t>& physical_config_indices,
MixedFilamentResult& out_result,
std::vector<DecomposeMissingComponent>& missing)
{
out_result = {};
missing.clear();
if (result.components.size() < 2) {
return false;
}
const bool standard_mode = result.mode == DecomposeMode::CMYW || result.mode == DecomposeMode::RYBW;
std::string basic_type;
std::string preset_name;
if (standard_mode) {
basic_type = decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
preset_name = find_decompose_standard_preset_name(source_config_idx, basic_type);
}
for (size_t i = 0; i < result.components.size(); ++i) {
const DecomposeComponent& comp = result.components[i];
out_result.ratios.push_back(comp.ratio);
if (!standard_mode) {
if (comp.filament_index <= 0) {
return false;
}
const size_t physical_idx = static_cast<size_t>(comp.filament_index - 1);
if (physical_idx >= physical_config_indices.size()) {
return false;
}
out_result.components.push_back(static_cast<unsigned int>(physical_config_indices[physical_idx] + 1));
continue;
}
if (comp.base_color == DecomposeBaseColor::None) {
return false;
}
DecomposeOfficialComponent official_component =
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
physical_config_indices, source_config_idx);
if (existing_idx > 0) {
out_result.components.push_back(static_cast<unsigned int>(existing_idx));
continue;
}
DecomposeMissingComponent missing_comp;
missing_comp.component_idx = out_result.components.size();
missing_comp.official_component = official_component;
missing_comp.preset_name = preset_name;
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
missing.push_back(std::move(missing_comp));
out_result.components.push_back(0);
}
const bool ok = out_result.components.size() == out_result.ratios.size() && out_result.components.size() >= 2;
return ok;
}
size_t count_decompose_new_physical_filaments(
const ColorDecomposeResult& result,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
size_t source_physical_idx,
const std::vector<size_t>* physical_config_indices)
{
if (result.mode != DecomposeMode::CMYW && result.mode != DecomposeMode::RYBW)
return 0;
std::vector<size_t> fallback_indices;
const std::vector<size_t>* indices = physical_config_indices;
if (!indices) {
fallback_indices.resize(physical_colors.size());
for (size_t i = 0; i < fallback_indices.size(); ++i)
fallback_indices[i] = i;
indices = &fallback_indices;
}
size_t source_config_idx = size_t(-1);
if (source_physical_idx < indices->size())
source_config_idx = (*indices)[source_physical_idx];
const std::string basic_type =
decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
size_t missing_count = 0;
for (const DecomposeComponent& comp : result.components) {
if (comp.base_color == DecomposeBaseColor::None)
continue;
DecomposeOfficialComponent official_component =
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
*indices, source_config_idx);
if (existing_idx <= 0)
++missing_count;
}
return missing_count;
}
bool confirm_create_decompose_missing_components(wxWindow* parent, const std::vector<DecomposeMissingComponent>& missing)
{
if (missing.empty())
return true;
static const char* config_key = "not_show_color_decompose_missing_component_tip";
if (wxGetApp().app_config->get(config_key) == "1") {
return true;
}
wxString missing_text;
for (size_t i = 0; i < missing.size(); ++i) {
if (i > 0)
missing_text += _L(", ");
missing_text += missing[i].display_name;
}
wxString message = _L("The current filament list does not contain ") + missing_text +
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
dlg.show_dsa_button();
int res = dlg.ShowModal();
if (res == wxID_OK && dlg.get_checkbox_state())
wxGetApp().app_config->set(config_key, "1");
return res == wxID_OK;
}
}} // namespace Slic3r::GUI
+104
View File
@@ -0,0 +1,104 @@
#ifndef slic3r_GUI_ColorDecomposeSupport_hpp_
#define slic3r_GUI_ColorDecomposeSupport_hpp_
#include <string>
#include <vector>
#include <wx/string.h>
#include <wx/colour.h>
#include "ColorDecomposeDialog.hpp"
class wxWindow;
namespace Slic3r {
class Preset;
namespace GUI {
// ---- Constants ----
inline constexpr const char* kDecomposePlaBasicType = "PLA Basic";
inline constexpr const char* kDecomposePetgBasicType = "PETG Basic";
inline constexpr const char* kDecomposePlaShortType = "PLA";
inline constexpr const char* kDecomposePetgShortType = "PETG";
inline constexpr const char* kDecomposePlaFilamentId = "GFA00";
inline constexpr const char* kDecomposePetgFilamentId = "GFG00";
inline constexpr const char* kDecomposeBambuPresetPrefix = "Bambu ";
// ---- Types ----
struct DecomposeOfficialComponent {
DecomposeBaseColor base_color{DecomposeBaseColor::None};
std::string color_hex;
std::string filament_id;
};
struct DecomposeMissingComponent {
size_t component_idx{0};
DecomposeOfficialComponent official_component;
std::string preset_name;
wxString display_name;
};
struct MixedFilamentResult;
// ---- Functions ----
std::string decompose_normalize_color_hex(std::string color);
const char* decompose_base_color_en(DecomposeBaseColor color);
wxString decompose_base_color_display(DecomposeBaseColor color);
std::string decompose_basic_type_from_source(size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_types);
std::string decompose_basic_filament_id(const std::string& basic_type);
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component);
DecomposeOfficialComponent lookup_decompose_official_component(
const std::string& basic_type,
DecomposeBaseColor base_color,
const wxColour& fallback);
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type);
// Returns "PLA Basic" / "PETG Basic" when preset_name names an official Bambu
// basic filament, else an empty string.
std::string official_basic_type_from_preset_name(const std::string& preset_name);
// Resolve display type for color-decompose: official Bambu Basic overrides
// get_filament_type when preset name matches; empty/missing -> "PLA".
std::string filament_type_for_color_decompose(Preset* preset);
int find_existing_decompose_component(
const DecomposeOfficialComponent& component,
const std::vector<std::string>& physical_colors,
const std::vector<size_t>& physical_config_indices,
size_t source_config_idx);
bool prepare_decompose_mixed_result(
const ColorDecomposeResult& result,
size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
const std::vector<size_t>& physical_config_indices,
MixedFilamentResult& out_result,
std::vector<DecomposeMissingComponent>& missing);
// For standard modes: how many base colors are not reusable from physical list.
// MaterialList returns 0. When physical_config_indices is null, indices are 0..n-1.
size_t count_decompose_new_physical_filaments(
const ColorDecomposeResult& result,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
size_t source_physical_idx,
const std::vector<size_t>* physical_config_indices);
bool confirm_create_decompose_missing_components(wxWindow* parent,
const std::vector<DecomposeMissingComponent>& missing);
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_ColorDecomposeSupport_hpp_
+57 -12
View File
@@ -577,22 +577,67 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
// BBS
static const char* keys[] = { "support_filament", "support_interface_filament"};
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
std::string key = std::string(keys[i]);
// Reset filament overrides pointing at a slot that no longer exists. Support and the wipe
// tower additionally reject mixed slots: the engine consumes those keys directly, so a virtual
// slot would reach the G-code unresolved, while the per-feature keys are resolved per layer.
static const char* physical_only_keys[] = { "support_filament", "support_interface_filament", "wipe_tower_filament" };
static const char* feature_keys[] = { "outer_wall_filament_id", "inner_wall_filament_id",
"sparse_infill_filament_id", "internal_solid_filament_id",
"top_surface_filament_id", "bottom_surface_filament_id" };
auto reset_invalid_filament = [this, config, filament_cnt](const char* key, bool allow_mixed) {
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (opt != nullptr) {
if (opt->getInt() > filament_cnt) {
DynamicPrintConfig new_conf = *config;
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
int new_value = 0;
if (conf_temp != nullptr && conf_temp->has(key)) {
new_value = conf_temp->opt_int(key);
if (opt == nullptr)
return;
const int val = opt->getInt();
const bool out_of_range = val > filament_cnt;
const bool is_mixed = !allow_mixed && val > 0 && val <= filament_cnt &&
wxGetApp().preset_bundle->is_mixed_filament(val - 1);
if (!out_of_range && !is_mixed)
return;
DynamicPrintConfig new_conf = *config;
int new_value = 0;
if (out_of_range) {
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
if (conf_temp != nullptr && conf_temp->has(key))
new_value = conf_temp->opt_int(key);
}
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
apply(config, &new_conf);
};
for (const char* key : physical_only_keys)
reset_invalid_filament(key, false);
for (const char* key : feature_keys)
reset_invalid_filament(key, true);
// Sub-layer splitting divides each layer by the mix ratio; an adaptive layer profile makes
// those sub-layer heights vary per layer, which degrades the blend. Warn once per enable.
{
static bool s_mixed_sublayer_warned = false;
bool sublayer_on = config->opt_bool("enable_mixed_color_sublayer");
if (sublayer_on && !s_mixed_sublayer_warned &&
wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
bool has_variable_layer = false;
for (const auto* obj : wxGetApp().model().objects) {
if (obj->layer_height_profile.get().size() > 4) {
has_variable_layer = true;
break;
}
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
apply(config, &new_conf);
}
if (has_variable_layer) {
MessageDialog dialog(m_msg_dlg_parent,
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
"", wxICON_WARNING | wxOK);
dialog.show_dsa_button();
is_msg_dlg_already_exist = true;
dialog.ShowModal();
is_msg_dlg_already_exist = false;
if (dialog.get_checkbox_state())
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
s_mixed_sublayer_warned = true;
}
}
if (!sublayer_on)
s_mixed_sublayer_warned = false;
}
if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None &&
+1
View File
@@ -27,6 +27,7 @@ void DevStatus::ParseStatus(const nlohmann::json& print_jj)
#else
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get exception=" << e.what();
#endif
(void)e; // suppress C4101 when BBL_RELEASE_TO_PUBLIC
}
}
@@ -26,8 +26,6 @@
#include "Widgets/HyperLink.hpp" // ORCA
#define DESIGN_INPUT_SIZE wxSize(FromDIP(100), -1)
namespace Slic3r {
namespace GUI {
+2 -2
View File
@@ -385,7 +385,7 @@ public:
wxWindow* window{ nullptr };
void BUILD() override;
/// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
void propagate_value() ;
void propagate_value() override;
void set_value(const std::string& value, bool change_event = false) {
m_disable_change_event = !change_event;
@@ -440,7 +440,7 @@ public:
wxWindow* window{ nullptr };
void BUILD() override;
// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
void propagate_value();
void propagate_value() override;
/* Under OSX: wxBitmapComboBox->GetWindowStyle() returns some weard value,
* so let use a flag, which has TRUE value for a control without wxCB_READONLY style
+184
View File
@@ -4,7 +4,10 @@
#include <cmath>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r { namespace GUI {
@@ -28,6 +31,113 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from,
}
}
static std::string to_hex(const wxColour& c)
{
return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString();
}
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights)
{
const size_t n = std::min(cols.size(), weights.size());
std::vector<std::string> hex_colors;
std::vector<int> int_weights;
hex_colors.reserve(n);
int_weights.reserve(n);
for (size_t i = 0; i < n; ++i) {
hex_colors.push_back(to_hex(cols[i]));
// Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi;
// only relative magnitude matters.
int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000.0)));
}
wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights));
return blended.IsOk() ? blended : wxColour(128, 128, 128);
}
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
const wxColour& second,
const Slic3r::GradientCurve& curve,
int steps)
{
std::vector<wxColour> ramp;
if (steps <= 0 || curve.points.size() < 2) return ramp;
ramp.reserve(steps);
for (int i = 0; i < steps; ++i) {
const double t = (steps > 1) ? (i + 0.5) / steps : 0.5;
const double r1 = Slic3r::sample_gradient_curve(curve, t);
ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1}));
}
return ramp;
}
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
// endpoints, otherwise the 0.10 -> 0.90 default.
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
{
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]);
if (custom.points.size() >= 2) return custom;
}
double start = kGradientMinRatio, end = kGradientMaxRatio;
const auto* range_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_range");
if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) {
CNumericLocalesSetter c_locale_setter;
float v0 = 0, v1 = 0;
if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 &&
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
start = v0;
end = v1;
}
}
Slic3r::GradientCurve curve;
curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}};
return curve;
}
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps)
{
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {};
if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {};
if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {};
if (slot >= comp_opt->values.size()) return {};
// Only two-component slots fade; anything else stays on the plain blended swatch.
const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]);
if (comp_ids.size() != 2) return {};
auto component_colour = [&](unsigned int id) {
wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour();
return c.IsOk() ? c : wxColour("#D9D9D9");
};
// Both gradient_range and the curve express the *first* component's ratio over Z, so
// the components stay in config order and the curve alone decides which end is which.
return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]),
mixed_gradient_curve(cfg, slot), steps);
}
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp)
{
if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int y = 0; y < rect.height; ++y) {
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
// Mapping over height - 1 puts both ends of the ramp on screen even in a short swatch.
const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5;
dc.SetBrush(wxBrush(ramp[static_cast<size_t>(t * (ramp.size() - 1) + 0.5)]));
dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1);
}
}
// Helper struct to hold bitmap and DC
struct BitmapDC {
wxBitmap bitmap;
@@ -47,6 +157,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) {
return BitmapDC(size);
}
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size)
{
if (ramp.empty()) return wxNullBitmap;
BitmapDC bdc = init_bitmap_dc(size);
if (!bdc.dc.IsOk()) return wxNullBitmap;
fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp);
bdc.dc.SelectObject(wxNullBitmap);
return bdc.bitmap;
}
// Check if a color is transparent (alpha == 0)
static bool is_transparent_color(const wxColour& color) {
return color.Alpha() == 0;
@@ -265,4 +388,65 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
}
}
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg)
{
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
const auto* ratio_opt = cfg.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
if (!is_mixed_opt || !comp_opt) return;
const size_t n = is_mixed_opt->values.size();
if (colors.size() < n) colors.resize(n);
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
const auto kFallback = wxColour(128, 128, 128, 255);
for (size_t i = 0; i < n; ++i) {
if (!is_mixed_opt->values[i]) continue;
if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; }
auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]);
if (comp_ids.empty()) { colors[i] = kFallback; continue; }
bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i];
std::vector<unsigned int> use_ids = comp_ids;
std::vector<int> weights;
if (is_gradient && comp_ids.size() >= 2) {
use_ids = { comp_ids.front(), comp_ids.back() };
weights = { 5000, 5000 };
} else {
auto ratios_d = Slic3r::parse_mixed_ratios(
(ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{},
comp_ids.size());
weights.reserve(comp_ids.size());
for (double r : ratios_d)
weights.push_back(static_cast<int>(std::lround(r * 10000.0)));
}
std::vector<std::string> hex_colors;
hex_colors.reserve(use_ids.size());
bool any_invalid = false;
for (unsigned int id : use_ids) {
if (id == 0 || id > colors.size()) { any_invalid = true; break; }
wxColour c = colors[id - 1];
if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) {
hex_colors.push_back(to_hex(c));
} else if (colour_opt && (id - 1) < colour_opt->values.size()) {
hex_colors.push_back(colour_opt->values[id - 1]);
} else {
any_invalid = true; break;
}
}
if (any_invalid) { colors[i] = kFallback; continue; }
std::string hex = Slic3r::blend_color_multi(hex_colors, weights);
wxColour blended(hex);
if (!blended.IsOk()) blended = kFallback;
colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255);
}
}
}} // namespace Slic3r::GUI
+35
View File
@@ -7,6 +7,10 @@
#include <wx/gdicmn.h>
#include <vector>
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
// force-included pch (the GUI test suite includes it directly).
namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
namespace Slic3r { namespace GUI {
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
@@ -28,6 +32,37 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors,
const wxSize& size,
bool force_gradient = false);
// Blend colours at the given relative weights through blend_color_multi, so a measured
// real-world mix is used where one exists instead of a plain channel lerp.
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights);
// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the
// model's height, the curve gives the first component's ratio at t, and the two
// components are blended at that ratio through blend_n_colors. Entry 0 is the bottom
// of the model, the last entry its top.
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
const wxColour& second,
const Slic3r::GradientCurve& curve,
int steps);
// Same ramp for a project config slot, resolving components, colours and curve (or the
// linear gradient_range fallback) from cfg. Returns empty for any slot that is not a
// two-component gradient mixed filament. steps is the ramp's resolution; pass the
// destination's height in pixels.
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
// Fill rect with a ramp, ramp.front() along the bottom edge.
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp.
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size);
// Recompute blended representative colors for mixed (virtual) filament slots.
// Reads mixed-filament config keys from cfg and writes back into colors[i]
// for every slot where filament_is_mixed[i] is true.
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg);
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_
+22 -1
View File
@@ -423,7 +423,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
if (properties_shown) {
float label_w = 0.0f;
float value_w = 0.0f;
properties_rows.reserve(13);
properties_rows.reserve(14);
auto add_row = [&properties_rows, &label_w, &value_w](std::string label, std::string value) {
label_w = std::max(label_w, ImGui::CalcTextSize(label.c_str()).x);
value_w = std::max(value_w, ImGui::CalcTextSize(value.c_str()).x);
@@ -436,6 +436,27 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
add_row(_u8L("Width"), buff);
if (is_extrusion) sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), vertex.height); else strcpy(buff, NA_CSTR);
add_row(_u8L("Height"), buff);
// ORCA: Length of the move ending at the current vertex. Arc moves (G2/G3) are discretized
// into several vertices sharing the same gcode line id, so accumulate the whole run to report
// the arc length instead of the length of a single chord.
if (vertex_id > 0 && (is_extrusion || vertex.is_travel() || vertex.is_wipe())) {
const size_t vertices_count = viewer->get_vertices_count();
size_t first_id = vertex_id;
while (first_id > 0 && viewer->get_vertex_at(first_id - 1).gcode_id == vertex.gcode_id)
--first_id;
size_t last_id = vertex_id;
while (last_id + 1 < vertices_count && viewer->get_vertex_at(last_id + 1).gcode_id == vertex.gcode_id)
++last_id;
float length = 0.0f;
for (size_t i = std::max<size_t>(first_id, 1); i <= last_id; ++i) {
length += (libvgcode::convert(viewer->get_vertex_at(i).position) -
libvgcode::convert(viewer->get_vertex_at(i - 1).position)).norm();
}
sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), length);
}
else
strcpy(buff, NA_CSTR);
add_row(_u8L("Length"), buff);
sprintf(buff, "%d", vertex.layer_id + 1);
add_row(_u8L("Layer"), buff);
sprintf(buff, ("%.1f " + _u8L("mm/s")).c_str(), vertex.feedrate);
+44 -2
View File
@@ -155,6 +155,11 @@ std::string& get_filament_mixture_warning_text(){
return filament_mixture_warning_text;
}
std::string& get_single_extruder_mixed_filament_warning_text(){
static std::string single_extruder_mixed_filament_warning_text;
return single_extruder_mixed_filament_warning_text;
}
static std::string format_number(float value)
{
@@ -2992,6 +2997,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
bool mix_pla_and_petg = cur_plate->check_mixture_of_pla_and_petg(full_config_temp);
_set_warning_notification(EWarning::MixUsePLAAndPETG, !mix_pla_and_petg);
bool single_extruder_mixed_risk = cur_plate->check_single_extruder_mixed_filament_risk(full_config_temp, get_single_extruder_mixed_filament_warning_text());
_set_warning_notification(EWarning::SingleExtruderMixedFilament, single_extruder_mixed_risk);
bool filament_nozzle_compatible = cur_plate->check_compatible_of_nozzle_and_filament(full_config_temp, wxGetApp().preset_bundle->filament_presets, get_nozzle_filament_incompatible_text());
_set_warning_notification(EWarning::NozzleFilamentIncompatible, !filament_nozzle_compatible);
@@ -3018,6 +3026,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
_set_warning_notification(EWarning::TPUPrintableError, false);
_set_warning_notification(EWarning::FilamentPrintableError, false);
_set_warning_notification(EWarning::MixUsePLAAndPETG, false);
_set_warning_notification(EWarning::SingleExtruderMixedFilament, false);
_set_warning_notification(EWarning::PrimeTowerOutside, false);
_set_warning_notification(EWarning::MultiExtruderPrintableError,false);
_set_warning_notification(EWarning::MultiExtruderHeightOutside,false);
@@ -9107,7 +9116,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
}
else {
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice()))
// A plate using a mixed filament whose components are broken cannot be sliced,
// so surface that on the plate toolbar the same way an unsliceable plate is.
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice())
|| wxGetApp().plater()->sidebar().has_broken_mixed_filament(plate_list.get_plate(i)))
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
else {
if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f)
@@ -9874,6 +9886,13 @@ void GLCanvas3D::_render_paint_toolbar() const
}
}
}
// ORCA: the loop above only labels a slot whose preset was found in the preset collection,
// while the render loop below iterates extruder_num. Pad the label arrays so a slot without a
// matching preset cannot index past them; a garbage std::string crashes ImGui::CalcTextSize.
while (int(filament_text_first_line.size()) < extruder_num) {
filament_text_first_line.emplace_back();
filament_text_second_line.emplace_back();
}
ImGuiWrapper& imgui = *wxGetApp().imgui();
const float canvas_w = float(get_canvas_size().get_width());
@@ -9903,6 +9922,10 @@ void GLCanvas3D::_render_paint_toolbar() const
bool disabled = !wxGetApp().plater()->can_fillcolor();
ColorRGBA rgba;
// Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than
// the single blended colour in `colors`. Every other slot's ramp is empty.
const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
for (int i = 0; i < extruder_num; i++) {
if (i > 0)
ImGui::SameLine();
@@ -9916,6 +9939,8 @@ void GLCanvas3D::_render_paint_toolbar() const
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max))
wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1));
}
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty())
ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]);
if (ImGui::IsItemHovered() && i < 9) {
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
@@ -9931,7 +9956,13 @@ void GLCanvas3D::_render_paint_toolbar() const
const float text_offset_y = 4.0f * em_unit * f_scale;
for (int i = 0; i < extruder_num; i++) {
decode_color(colors[i], rgba);
// A gradient slot's swatch shows its fade instead of the blended colour in `colors`, so the
// labels take their contrast from the colour printed at the middle of the fade they sit on.
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) {
const wxColour& c = gradient_ramps[i][gradient_ramps[i].size() / 2];
rgba = ColorRGBA(c.Red(), c.Green(), c.Blue(), c.Alpha());
} else
decode_color(colors[i], rgba);
float gray = 0.299 * rgba.r_uchar() + 0.587 * rgba.g_uchar() + 0.114 * rgba.b_uchar();
ImVec4 text_color = gray < 80 ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0, 0, 0, 1.0f);
@@ -10775,6 +10806,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
case EWarning::MixUsePLAAndPETG:
text = _u8L("PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality.");
break;
case EWarning::SingleExtruderMixedFilament:
text = get_single_extruder_mixed_filament_warning_text();
break;
case EWarning::PrimeTowerOutside:
text = _u8L("The prime tower extends beyond the plate boundary.");
break;
@@ -10823,6 +10857,14 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLNozzleFilamentIncompatible, NotificationLevel::WarningNotificationLevel);
}
}
else if (warning == EWarning::SingleExtruderMixedFilament) {
// Close by type: check_single_extruder_mixed_filament_risk() clears the shared text
// buffer on every call, so a close-by-text would miss once the risk is gone.
if (state)
notification_manager.push_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel, text);
else
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel);
}
else {
if (state)
notification_manager.push_plater_warning_notification(text);
+1
View File
@@ -391,6 +391,7 @@ class GLCanvas3D
PrimeTowerOutside,
NozzleFilamentIncompatible,
MixtureFilamentIncompatible,
SingleExtruderMixedFilament,
FlushingVolumeZero
};
+2
View File
@@ -18,7 +18,9 @@
#import <IOKit/pwr_mgt/IOPMLib.h>
#elif _WIN32
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include "boost/nowide/convert.hpp"
#endif
+7 -3
View File
@@ -521,10 +521,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
/* FT_GCODE */ { L("G-code files"), { ".gcode"sv} },
#ifdef __APPLE__
/* FT_MODEL */
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
#else
/* FT_MODEL */
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}},
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}},
#endif
/* FT_ZIP */ { L("ZIP files"), { ".zip"sv } },
/* FT_PROJECT */ { L("Project files"), { ".3mf"sv} },
@@ -8905,7 +8905,11 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch
if (printer_technology == ptFFF && !edited_printer_preset.config.opt_bool("single_extruder_multi_material")) {
auto* nozzle_diameter = edited_printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter");
if (nozzle_diameter) {
preset_bundle->set_num_filaments(nozzle_diameter->values.size());
// Mixed-color slots are virtual filaments kept at the tail of the list, so they have no
// nozzle of their own. Sizing to the nozzle count alone would silently drop the mixes of
// a just-loaded project, and update_extruder_count() would then strip the facets painted
// with them.
preset_bundle->set_num_filaments(nozzle_diameter->values.size() + preset_bundle->num_mixed_filaments());
}
}
this->plater()->set_printer_technology(printer_technology);
+14 -10
View File
@@ -1656,16 +1656,16 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
{
wxMenu *menu = &m_filament_action_menu;
if (init) {
// ORCA rebuild menu everytime instead checking existing of every item then deleting
while (menu->GetMenuItemCount() > 0)
menu->Destroy(menu->FindItemByPosition(0));
//if (init) { //
append_menu_item(
menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) {
plater()->sidebar().edit_filament(); }, "", nullptr,
[]() { return true; }, m_parent);
}
const int item_id = menu->FindItem(_L("Merge with"));
if (item_id != wxNOT_FOUND)
menu->Destroy(item_id);
//}
wxMenu* sub_menu = new wxMenu();
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
@@ -1684,11 +1684,15 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "",
[filaments_cnt]() { return filaments_cnt > 1; }, m_parent);
// Decompose a target colour into a printable mix of the loaded filaments. Placed before the
append_menu_item(
menu, wxID_ANY, _L("Decompose Color"), "", [](wxCommandEvent&) {
plater()->sidebar().decompose_filament_color(kSidebarContextMenuFilamentId); }, "", nullptr,
[]() { return plater()->sidebar().combos_filament().size() >= 2; }, m_parent);
menu->AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete
// ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS
const int delete_id = menu->FindItem(_L("Delete"));
if (delete_id != wxNOT_FOUND)
menu->Destroy(delete_id);
append_menu_item(
menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) {
plater()->sidebar().delete_filament(-2); }, "", nullptr,
+18
View File
@@ -3233,6 +3233,24 @@ void ObjectList::merge(bool to_multipart_object)
void ObjectList::layers_editing()
{
// Height ranges give each range its own layer height, varying the mixed sub-layer heights just
// like an adaptive profile, so this raises the same warning as variable layer height and shares
// its do-not-show-again flag.
const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
if (print_config.opt_bool("enable_mixed_color_sublayer")) {
if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
// Orca: parent to the plater like the sibling site in Plater::priv::on_action_layersediting
// (BBS passes nullptr, which MsgDialog remaps to the main frame).
MessageDialog dlg(wxGetApp().plater(),
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.show_dsa_button();
dlg.ShowModal();
if (dlg.get_checkbox_state())
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
}
}
const Selection& selection = scene_selection();
const int obj_idx = selection.get_object_idx();
wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ?
+1 -1
View File
@@ -85,7 +85,7 @@ public:
void update_model_object();
//ClippingPlane get_sla_clipping_plane() const;
bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); }
bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); }
bool wants_enter_leave_snapshots() const override { return true; }
std::string get_gizmo_entering_text() const override { return _u8L("Entering Brim Ears"); }
-1
View File
@@ -597,7 +597,6 @@ void GLGizmoMeasure::on_render()
}
}
Vec3d position_on_model;
Vec3d direction_on_model;
size_t model_facet_idx = -1;
double closest_hit_distance = std::numeric_limits<double>::max();
{
+1 -1
View File
@@ -75,7 +75,7 @@ protected:
virtual void on_render() override;
virtual void on_set_state() override;
virtual CommonGizmosDataID on_get_requirements() const override;
virtual void on_render_input_window(float x, float y, float bottom_limit);
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
void on_load(cereal::BinaryInputArchive &ar) override;
void on_save(cereal::BinaryOutputArchive &ar) const override;
@@ -78,6 +78,9 @@ void GLGizmoMmuSegmentation::init_extruders_data()
m_extruders_colors = wxGetApp().plater()->get_extruders_colors();
m_selected_extruder_idx = 0;
m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
m_gradient_ramps.resize(m_extruders_colors.size());
// keep remap table consistent with current extruder count
m_extruder_remap.resize(m_extruders_colors.size());
for (size_t i = 0; i < m_extruder_remap.size(); ++i)
@@ -305,15 +308,32 @@ void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y)
}
// ORCA
bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
{
// Inset of the frame stroked below, which is what trims the swatch down to its visible shape.
const float frame_inset = 1.5f;
ImDrawList* draw_list = ImGui::GetWindowDrawList();
std::string label_id = std::to_string(idx) + id_str + std::to_string(idx);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 size = ImVec2(27.f * scale, 27.f * scale);
ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color);
ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1));
bool dark_tone = (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
// Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade.
const std::vector<wxColour>* gradient = gradient_of(idx - 1);
// The centered slot number sits at the swatch's mid height, so take its contrast from the colour
// printed there rather than from the slot's blended color.
bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 :
(0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
// Paint a gradient mixed filament's fade before the button and keep the button transparent, so
// the slot number and the frame below stay on top of it. The bands cannot round their corners,
// so the fade is inset to the frame, which masks it into the shape a plain color slot gets.
if (gradient) {
ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale},
{pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient);
color_vec.w = 0.f; // let the fade show through
}
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding , 7.f * scale);
@@ -329,7 +349,7 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, cons
auto drawBorder = [&](float d, float r, float t, ImU32 col) {
draw_list->AddRect({pos.x + d * scale, pos.y + d * scale}, {pos.x + size.x - d * scale , pos.y + size.y - d * scale}, col, r * scale, 0, t * scale);
};
drawBorder(1.5f, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
drawBorder(frame_inset, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
if(active)
drawBorder(.5f, 4.f , 2.f, br_color);
else
@@ -433,7 +453,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
m_selected_extruder_idx = extruder_idx;
}
if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
}
// ORCA: Remap filaments section (Border only, Title in border).
// Styled as a panel for visual grouping.
@@ -731,6 +751,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
continue;
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
// A volume may be assigned to a mixed-color slot, whose index can sit past the
// physical colour list; fall back to the first colour rather than reading OOB.
if (extruder_idx >= (int)m_extruders_colors.size())
extruder_idx = 0;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
@@ -753,6 +777,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors()
TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
int extruder_idx = m_volumes_extruder_idxs[i];
int extruder_color_idx = std::max(0, extruder_idx - 1);
// A mixed-color slot can index past the physical colour list; fall back to the first colour.
if (extruder_color_idx >= (int)m_extruders_colors.size())
extruder_color_idx = 0;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
@@ -73,11 +73,10 @@ public:
void data_changed(bool is_serializing) override;
// TriangleSelector::serialization/deserialization has a limit to store 19 different states.
// EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored.
// When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization
// will be also extended to support additional states, requiring at least one state to remain free out of 19 states.
static const constexpr size_t EXTRUDERS_LIMIT = 16;
// The paint material limit follows EnforcerBlockerType::ExtruderMax: TriangleSelector
// serialization covers the extended (17..32) range through an escape nibble. Mixed-color
// filaments occupy ordinary slots, so they draw from the same budget as physical ones.
static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
const float get_cursor_radius_min() const override { return CursorRadiusMin; }
@@ -116,6 +115,10 @@ protected:
// Filament remap feature
std::vector<size_t> m_extruder_remap; // index → target extruder index
// Colours each gradient mixed filament actually prints, bottom of the model first, mirrored
// from Plater so the extruder swatches draw the same fade the editor previews. Plain
// filament slots keep an empty ramp.
std::vector<std::vector<wxColour>> m_gradient_ramps;
// ORCA: Cache used filaments to filter UI
std::set<size_t> m_used_filaments; // Set of used filament indices (cached)
@@ -137,7 +140,13 @@ private:
void init_model_triangle_selectors();
// ORCA
bool draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
// Gradient ramp of a filament slot, or nullptr when the slot is a plain single color
// filament. A non-null result is never empty.
const std::vector<wxColour>* gradient_of(int idx) const
{
return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr;
}
// BBS
void update_triangle_selectors_colors();

Some files were not shown because too many files have changed in this diff Show More