BBL Port Color Mix Base

This commit is contained in:
Ian Bassi
2026-08-23 22:11:48 +08:00
committed by SoftFever
parent 550e234a37
commit 8fea099d99
75 changed files with 34174 additions and 51 deletions
File diff suppressed because it is too large Load Diff
+13 -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,7 @@ set(lisbslic3r_sources
format.hpp
Format/OBJ.cpp
Format/OBJ.hpp
Format/ResourcePathUtils.hpp
Format/objparser.cpp
Format/objparser.hpp
Format/SL1.cpp
@@ -549,7 +561,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
+361
View File
@@ -0,0 +1,361 @@
#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 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);
}
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);
};
std::vector<std::string> norm_hexes;
norm_hexes.reserve(component_hexes.size());
for (const auto& h : component_hexes) {
std::string n = normalize_hex(h);
if (n.empty())
return {};
norm_hexes.push_back(std::move(n));
}
for (const StandardRecipeEntry& entry : standard_entries()) {
if (entry.source != "measured" && entry.source != "interpolated")
continue;
if (entry.component_hexes.size() != norm_hexes.size())
continue;
if (entry.ratios != ratios)
continue;
bool match = true;
for (size_t i = 0; i < norm_hexes.size(); ++i) {
if (normalize_hex(entry.component_hexes[i]) != norm_hexes[i]) {
match = false;
break;
}
}
if (match)
return entry.measured_hex;
}
return {};
}
} // 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
+554
View File
@@ -0,0 +1,554 @@
#include "FilamentMixer.hpp"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <limits>
#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);
}
}
} // namespace Slic3r
+144
View File
@@ -0,0 +1,144 @@
#ifndef SLIC3R_FILAMENT_MIXER_HPP
#define SLIC3R_FILAMENT_MIXER_HPP
#include <limits>
#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);
} // 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
+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 + decoded texture images) 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
View File
@@ -394,6 +394,7 @@ 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': {
+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);
+323 -2
View File
@@ -6557,6 +6557,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's 混色耗材 feature; adapted to Orca's InstanceVisit-based
// 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;
gcode += m_writer.travel_to_z(obj_sub_z, "restore Z for support");
}
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;
gcode += m_writer.travel_to_z(print_z, "restore Z after sublayers");
}
}
if (first_layer) {
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
@@ -7634,6 +7946,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;
@@ -7933,8 +8254,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
@@ -747,6 +747,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;
+691
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>
@@ -402,7 +409,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 +431,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 +445,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 +455,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 +740,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 +781,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 +791,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 +860,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++;
}
@@ -1945,6 +2048,594 @@ 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;
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
// explicitly NOT split per-volume in v1 per the design doc) keep its legacy
// 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;
+82
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;
@@ -299,6 +362,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);
@@ -313,6 +378,23 @@ private:
std::vector<unsigned int> m_all_printing_extruders;
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();
+37 -5
View File
@@ -1,6 +1,7 @@
#include "Model.hpp"
#include "libslic3r.h"
#include "BuildVolume.hpp"
#include "TexturePainting.hpp"
#include "ClipperUtils.hpp"
#include "Exception.hpp"
#include "Model.hpp"
@@ -104,6 +105,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 +141,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;
@@ -281,8 +284,21 @@ 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){
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. Replaces Orca's previous "not implemented"
// placeholder for this branch.
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){
ObjDialogInOut in_out;
in_out.model = &model;
in_out.lost_material_name = obj_info.lost_material_name;
@@ -578,6 +594,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 +2593,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 +2605,13 @@ 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();
if (eid > extruder_count) {
// A mixed-color slot is virtual and legitimately sits past the physical filament count,
// so an assignment to one is not stale and must survive the delete.
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 +3518,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;
+1
View File
@@ -1183,6 +1183,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",
+153 -1
View File
@@ -7,6 +7,7 @@
#include "PresetCacheFormat.hpp"
#include "PrintConfig.hpp"
#include "FilamentMixer.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
#include "Utils.hpp"
@@ -71,7 +72,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
@@ -2704,6 +2715,40 @@ void PresetBundle::load_installed_sla_materials(AppConfig &config)
preset.set_visible_from_appconfig(config);
}
// Restore the mixed-color filament metadata written by export_selections(). Every array is
// resized to the filament count so a project saved with a different filament count, or one
// predating these keys, still yields well-formed parallel arrays.
static void load_mixed_filament_settings(DynamicPrintConfig &project_config, AppConfig &config,
const std::string &printer_name, size_t n_filaments)
{
std::vector<std::string> parts;
auto load_bools = [&](const char *key, const char *opt_key) {
auto &vals = project_config.option<ConfigOptionBools>(opt_key)->values;
if (config.has_printer_setting(printer_name, key)) {
boost::algorithm::split(parts, config.get_printer_setting(printer_name, key), boost::algorithm::is_any_of(","));
vals.clear();
for (const auto &p : parts) vals.push_back(p == "1");
}
vals.resize(n_filaments, false);
};
auto load_strings = [&](const char *key, const char *opt_key) {
auto &vals = project_config.option<ConfigOptionStrings>(opt_key)->values;
if (config.has_printer_setting(printer_name, key)) {
boost::algorithm::split(parts, config.get_printer_setting(printer_name, key), boost::algorithm::is_any_of("|"));
vals = parts;
}
vals.resize(n_filaments, std::string{});
};
load_bools("filament_is_mixed", "filament_is_mixed");
load_strings("filament_mixed_components", "filament_mixed_components");
load_strings("filament_mixed_sublayer_ratios", "filament_mixed_sublayer_ratios");
load_bools("filament_mixed_gradient", "filament_mixed_gradient");
load_strings("filament_mixed_gradient_range", "filament_mixed_gradient_range");
load_strings("filament_mixed_gradient_curve", "filament_mixed_gradient_curve");
load_bools("filament_mixed_gradient_per_part", "filament_mixed_gradient_per_part");
}
void PresetBundle::update_selections(AppConfig &config)
{
std::string initial_printer_profile_name = printers.get_selected_preset_name();
@@ -2784,6 +2829,7 @@ 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());
}
load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size());
// 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,
@@ -2934,6 +2980,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());
// 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,
@@ -3068,6 +3115,31 @@ void PresetBundle::export_selections(AppConfig &config)
"|");
config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str);
// Mixed-color filament metadata. Bools are joined with ',' and strings with '|' because
// the component/ratio/curve strings themselves contain commas.
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", boost::algorithm::join(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());
@@ -3103,6 +3175,24 @@ 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. Missing this leaves the
// arrays short and every lookup of a newly created slot reads past the end.
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()) {
@@ -3143,6 +3233,24 @@ 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. Missing this leaves the
// arrays short and every lookup of a newly created slot reads past the end.
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()) {
@@ -3215,9 +3323,53 @@ 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];
}
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)
{
+3
View File
@@ -497,6 +497,9 @@ 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;
void on_extruders_count_changed(int extruder_count);
+21
View File
@@ -2719,6 +2719,19 @@ 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).
if (m_config.print_sequence != PrintSequence::ByObject && !tool_ordering.layer_tools().empty()) {
const LayerTools &first_lt = tool_ordering.layer_tools().front();
for (auto &[obj_id, ext_1based] : objectExtruderMap) {
if (ext_1based == 0)
continue;
auto it = first_lt.mixed_filament_resolution.find(ext_1based - 1);
if (it != first_lt.mixed_filament_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();
@@ -3776,6 +3789,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;
}
+15 -2
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() {
+122 -9
View File
@@ -1,6 +1,7 @@
#include "ClipperUtils.hpp"
#include "Model.hpp"
#include "Print.hpp"
#include "FilamentMixer.hpp"
#include <boost/log/trivial.hpp>
#include <cfloat>
@@ -886,7 +887,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;
}
@@ -978,7 +984,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>();
@@ -1013,19 +1022,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];
@@ -1034,9 +1095,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()) {
@@ -1121,6 +1184,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();
}
@@ -1141,6 +1210,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());
@@ -1802,6 +1882,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.
@@ -1862,6 +1965,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.
@@ -1884,7 +1996,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) {
+64
View File
@@ -3263,6 +3263,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
@@ -7402,6 +7458,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.");
+9
View File
@@ -1538,6 +1538,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))
@@ -1838,6 +1846,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
+663
View File
@@ -0,0 +1,663 @@
#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;
}
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
+115
View File
@@ -0,0 +1,115 @@
#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(); }
};
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);
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
@@ -0,0 +1,789 @@
#include "TextureToColor.hpp"
#include <tbb/parallel_for.h>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <functional>
#include <set>
#include <boost/next_prior.hpp>
#include "CgalUtils.hpp"
#include "ColorUtils.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <filesystem>
#include <fstream>
#include "Repair.hpp"
#include "libslic3r/AABBTreeIndirect.hpp"
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace tex2color {
using namespace color_utils;
// #define OUTPUT_TEST_RESULT
static void SaveToOFF(const std::string& path, const TriMesh& mesh, const std::vector<RGB>& face_colors)
{
std::filesystem::create_directories(std::filesystem::path(path).parent_path());
std::ofstream ofs(path);
if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "SaveToOFF: failed to open " << path;
return;
}
const auto& vertices = mesh.vertices;
const auto& faces = mesh.indices;
ofs << "OFF\n";
ofs << vertices.size() << " " << faces.size() << " 0\n";
for (const auto& v : vertices) {
ofs << v.x() << " " << v.y() << " " << v.z() << "\n";
}
for (std::size_t i = 0; i < faces.size(); ++i) {
const auto& f = faces[i];
ofs << "3 " << f[0] << " " << f[1] << " " << f[2];
if (i < face_colors.size()) {
ofs << " " << face_colors[i][0] / 255.0
<< " " << face_colors[i][1] / 255.0
<< " " << face_colors[i][2] / 255.0
<< " 1.0";
}
ofs << "\n";
}
}
static std::vector<std::size_t> count_cluster_label_usage(const std::vector<std::size_t>& face_labels, std::size_t cluster_count)
{
std::vector<std::size_t> usage(cluster_count, 0);
for (std::size_t label : face_labels) {
if (label < cluster_count) {
++usage[label];
}
}
return usage;
}
static bool discard_unused_cluster_centers(std::vector<RGB>& cluster_centers, std::vector<std::size_t>& face_labels, const char* stage_name)
{
if (cluster_centers.empty()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot discard unused cluster centers at " << stage_name
<< ", no cluster center is available.";
return false;
}
const std::vector<std::size_t> usage = count_cluster_label_usage(face_labels, cluster_centers.size());
std::vector<std::size_t> label_remap(cluster_centers.size(), std::numeric_limits<std::size_t>::max());
std::vector<RGB> used_cluster_centers;
used_cluster_centers.reserve(cluster_centers.size());
for (std::size_t cluster_id = 0; cluster_id < cluster_centers.size(); ++cluster_id) {
if (usage[cluster_id] == 0) {
continue;
}
label_remap[cluster_id] = used_cluster_centers.size();
used_cluster_centers.push_back(cluster_centers[cluster_id]);
}
if (used_cluster_centers.size() == cluster_centers.size()) {
return true;
}
if (used_cluster_centers.empty()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot discard unused cluster centers at " << stage_name
<< ", no face uses any valid cluster center.";
return false;
}
for (std::size_t& label : face_labels) {
if (label >= label_remap.size() || label_remap[label] == std::numeric_limits<std::size_t>::max()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot remap cluster label " << label
<< " at " << stage_name << ".";
return false;
}
label = label_remap[label];
}
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: discarded " << (cluster_centers.size() - used_cluster_centers.size())
<< " unused adaptive cluster centers at " << stage_name << ".";
cluster_centers = std::move(used_cluster_centers);
return true;
}
static bool ensure_all_cluster_centers_used(const std::vector<RGB>& source_face_colors, const std::vector<RGB>& cluster_centers,
std::vector<std::size_t>& face_labels, const char* stage_name)
{
if (source_face_colors.size() != face_labels.size()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot preserve cluster colors at " << stage_name
<< ", face color count does not match label count.";
return false;
}
if (cluster_centers.empty()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot preserve cluster colors at " << stage_name
<< ", no cluster center is available.";
return false;
}
if (cluster_centers.size() > face_labels.size()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot use all cluster centers at " << stage_name
<< ", centers=" << cluster_centers.size() << " faces=" << face_labels.size() << ".";
return false;
}
std::vector<std::size_t> usage = count_cluster_label_usage(face_labels, cluster_centers.size());
std::size_t missing_count = 0;
for (std::size_t cluster_id = 0; cluster_id < usage.size(); ++cluster_id) {
if (usage[cluster_id] != 0) {
continue;
}
++missing_count;
double best_cost = std::numeric_limits<double>::max();
std::size_t best_face_id = std::numeric_limits<std::size_t>::max();
std::size_t best_old_cluster_id = std::numeric_limits<std::size_t>::max();
for (std::size_t fid = 0; fid < face_labels.size(); ++fid) {
const std::size_t old_cluster_id = face_labels[fid];
if (old_cluster_id >= cluster_centers.size() || usage[old_cluster_id] <= 1) {
continue;
}
const double old_dist = calc_rgb_color_difference_by_ciede2000(source_face_colors[fid], cluster_centers[old_cluster_id]);
const double new_dist = calc_rgb_color_difference_by_ciede2000(source_face_colors[fid], cluster_centers[cluster_id]);
const double cost = new_dist - old_dist;
if (cost < best_cost) {
best_cost = cost;
best_face_id = fid;
best_old_cluster_id = old_cluster_id;
}
}
if (best_face_id == std::numeric_limits<std::size_t>::max()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: failed to assign a seed face for unused cluster " << cluster_id
<< " at " << stage_name << ".";
continue;
}
face_labels[best_face_id] = cluster_id;
--usage[best_old_cluster_id];
++usage[cluster_id];
}
if (missing_count > 0) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: reassigned seed faces for " << missing_count
<< " unused cluster centers at " << stage_name << ".";
}
for (std::size_t count : usage) {
if (count == 0) {
return false;
}
}
return true;
}
// Bilinear interpolation texture sampling; sub-pixel precision avoids nearest-neighbor aliasing
static RGB get_pixel_color(float u, float v, const cv::Mat& texture) {
u = u - std::floor(u);
v = v - std::floor(v);
// glTF UV convention: (0,0) = top-left, v increases downward
float fx = u * (texture.cols - 1);
float fy = v * (texture.rows - 1);
int x0 = std::clamp(static_cast<int>(fx), 0, texture.cols - 1);
int y0 = std::clamp(static_cast<int>(fy), 0, texture.rows - 1);
int x1 = std::min(x0 + 1, texture.cols - 1);
int y1 = std::min(y0 + 1, texture.rows - 1);
float wx = fx - x0;
float wy = fy - y0;
const int ch = texture.channels();
auto sample = [&](int row, int col) -> std::array<float, 3> {
const uchar* ptr = texture.data + row * texture.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);
// Bilinear blend: lerp(lerp(c00,c10,wx), lerp(c01,c11,wx), wy)
RGB color;
for (int i = 0; i < 3; ++i) {
float top = c00[i] * (1.0f - wx) + c10[i] * wx;
float bot = c01[i] * (1.0f - wx) + c11[i] * wx;
color[i] = static_cast<std::size_t>(std::clamp(top * (1.0f - wy) + bot * wy, 0.0f, 255.0f));
}
return color;
}
// 7-point triangular Gaussian quadrature barycentric coordinates and weights (precision sufficient for capturing texture detail within faces)
static constexpr std::array<std::array<float, 3>, 7> GAUSS_TRI_BARY = {{
{1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f},
{0.059715871f, 0.470142064f, 0.470142064f},
{0.470142064f, 0.059715871f, 0.470142064f},
{0.470142064f, 0.470142064f, 0.059715871f},
{0.797426985f, 0.101286507f, 0.101286507f},
{0.101286507f, 0.797426985f, 0.101286507f},
{0.101286507f, 0.101286507f, 0.797426985f},
}};
static constexpr std::array<float, 7> GAUSS_TRI_WEIGHT = {0.225f, 0.132394152f, 0.132394152f, 0.132394152f, 0.125939181f, 0.125939181f, 0.125939181f};
static_assert(
[]() constexpr {
float sum = 0.0f;
for (auto w : GAUSS_TRI_WEIGHT) {
sum += w;
}
return sum > 0.999f && sum < 1.001f;
}(),
"Sum of Gaussian quadrature weights must be 1.0");
// Multi-point Gaussian quadrature sampling on a single face; returns weighted average color.
// GAUSS_TRI_WEIGHT sums to 1.0 (Hammer quadrature formula), no normalization needed.
static RGB sample_face_color(const std::array<Vec2f, 3>& uvs, const cv::Mat& texture) {
float r = 0.0f, g = 0.0f, b = 0.0f;
for (int k = 0; k < 7; ++k) {
float u = GAUSS_TRI_BARY[k][0] * uvs[0].x() + GAUSS_TRI_BARY[k][1] * uvs[1].x() + GAUSS_TRI_BARY[k][2] * uvs[2].x();
float v = GAUSS_TRI_BARY[k][0] * uvs[0].y() + GAUSS_TRI_BARY[k][1] * uvs[1].y() + GAUSS_TRI_BARY[k][2] * uvs[2].y();
RGB c = get_pixel_color(u, v, texture);
float w = GAUSS_TRI_WEIGHT[k];
r += w * c[0];
g += w * c[1];
b += w * c[2];
}
return RGB{static_cast<std::size_t>(std::clamp(r, 0.0f, 255.0f)), static_cast<std::size_t>(std::clamp(g, 0.0f, 255.0f)),
static_cast<std::size_t>(std::clamp(b, 0.0f, 255.0f))};
}
// Use array<Vec2f,3> instead of vector<Vec2f> for UV storage to avoid per-face heap allocations at million-face scale
using FaceUVArray = std::array<Vec2f, 3>;
static bool linear_subdivision(TriMesh& mesh, std::vector<FaceUVArray>& uv_coords, const std::function<void(int)>& sub_progress = nullptr) {
const auto& original_vertices = mesh.vertices;
const auto& original_faces = mesh.indices;
TriVertices sub_vertices = mesh.vertices;
sub_vertices.reserve(original_vertices.size() + original_faces.size() * 3);
TriFaces sub_faces;
std::vector<FaceUVArray> sub_uv_coords;
// Single-level flat map with edge key encoding replaces nested unordered_map;
// merges two vertex indices into a single uint64_t to reduce hash lookups and indirection.
if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] {
BOOST_LOG_TRIVIAL(warning) << "[boundary] " << __FUNCTION__ << " vertex_count=" << original_vertices.size() << " exceeds 32-bit edge_key encoding range, skipping subdivision";
return false;
}
auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t {
return a < b ? ((static_cast<uint64_t>(a) << 32) | b) : ((static_cast<uint64_t>(b) << 32) | a);
};
std::unordered_map<uint64_t, std::size_t> map_edge_to_sub_vtx;
map_edge_to_sub_vtx.reserve(original_faces.size() * 3 / 2);
for (const auto& face : original_faces) {
for (std::size_t i = 0; i < 3; ++i) {
std::size_t vtx_1 = face[i];
std::size_t vtx_2 = face[(i + 1) % 3];
uint64_t key = edge_key(vtx_1, vtx_2);
if (map_edge_to_sub_vtx.count(key) > 0) {
continue;
}
TriVertex edge_vtx = (original_vertices[vtx_1] + original_vertices[vtx_2]) * 0.5;
map_edge_to_sub_vtx[key] = sub_vertices.size();
sub_vertices.push_back(edge_vtx);
}
}
if (sub_progress) {
sub_progress(50);
}
// Subdivide faces and their UVs: each original face splits into 4 sub-faces (parallel writes, no contention)
const std::size_t N = original_faces.size();
sub_faces.resize(N * 4);
sub_uv_coords.resize(N * 4);
std::atomic<bool> has_missing_edge{false};
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, N), [&](const tbb::blocked_range<size_t>& range) {
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
const std::size_t base = fid * 4;
const auto& face = original_faces[fid];
std::size_t vtx_0 = face[0];
std::size_t vtx_1 = face[1];
std::size_t vtx_2 = face[2];
auto it01 = map_edge_to_sub_vtx.find(edge_key(vtx_0, vtx_1));
auto it12 = map_edge_to_sub_vtx.find(edge_key(vtx_1, vtx_2));
auto it20 = map_edge_to_sub_vtx.find(edge_key(vtx_2, vtx_0));
if (it01 == map_edge_to_sub_vtx.end() || it12 == map_edge_to_sub_vtx.end() || it20 == map_edge_to_sub_vtx.end()) [[unlikely]] {
has_missing_edge.store(true, std::memory_order_relaxed);
Vec3i32 degen(vtx_0, vtx_0, vtx_0);
FaceUVArray degen_uv = {uv_coords[fid][0], uv_coords[fid][0], uv_coords[fid][0]};
for (int k = 0; k < 4; ++k) {
sub_faces[base + k] = degen;
sub_uv_coords[base + k] = degen_uv;
}
continue;
}
std::size_t e01 = it01->second;
std::size_t e12 = it12->second;
std::size_t e20 = it20->second;
const Vec2f& uv0 = uv_coords[fid][0];
const Vec2f& uv1 = uv_coords[fid][1];
const Vec2f& uv2 = uv_coords[fid][2];
Vec2f uv_e01 = (uv0 + uv1) * 0.5f;
Vec2f uv_e12 = (uv1 + uv2) * 0.5f;
Vec2f uv_e20 = (uv2 + uv0) * 0.5f;
sub_faces[base + 0] = Vec3i32(vtx_0, e01, e20);
sub_uv_coords[base + 0] = {uv0, uv_e01, uv_e20};
sub_faces[base + 1] = Vec3i32(e01, vtx_1, e12);
sub_uv_coords[base + 1] = {uv_e01, uv1, uv_e12};
sub_faces[base + 2] = Vec3i32(e01, e12, e20);
sub_uv_coords[base + 2] = {uv_e01, uv_e12, uv_e20};
sub_faces[base + 3] = Vec3i32(e20, e12, vtx_2);
sub_uv_coords[base + 3] = {uv_e20, uv_e12, uv2};
}
});
// Remove degenerate triangles (three identical vertices) to avoid impacting downstream SDF / Remesh steps
if (has_missing_edge.load(std::memory_order_relaxed)) {
std::size_t write_idx = 0;
for (std::size_t i = 0; i < sub_faces.size(); ++i) {
if (sub_faces[i][0] == sub_faces[i][1] && sub_faces[i][1] == sub_faces[i][2]) {
continue;
}
if (write_idx != i) {
sub_faces[write_idx] = sub_faces[i];
sub_uv_coords[write_idx] = sub_uv_coords[i];
}
++write_idx;
}
BOOST_LOG_TRIVIAL(warning) << "[warning] linear_subdivision has missing edge vertex, removed " << (sub_faces.size() - write_idx) << " degenerate triangles";
sub_faces.resize(write_idx);
sub_uv_coords.resize(write_idx);
}
if (sub_progress) {
sub_progress(100);
}
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: input faces count = " << mesh.indices.size() << ".";
mesh = TriMesh(sub_faces, sub_vertices);
uv_coords = std::move(sub_uv_coords);
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: output faces count = " << mesh.indices.size() << ".";
return true;
}
bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<Vec2f>>& texture_mesh_uv_coords, const cv::Mat& texture, TriMesh& color_mesh,
std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings, AlgoProgressCallback progress_callback,
AlgoCancelCallback cancel_callback) {
auto report = [&](int pct, const char* msg) {
if (progress_callback) {
progress_callback({pct, msg});
}
};
auto sub_report = [&](int sub_pct, int range_start, int range_end, const char* msg) {
int pct = range_start + sub_pct * (range_end - range_start) / 100;
report(pct, msg);
};
auto cancelled = [&]() -> bool {
if (cancel_callback && cancel_callback()) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled";
return true;
}
return false;
};
color_mesh.clear();
face_colors.clear();
report(0, "Initializing");
if (cancelled()) {
return false;
}
if (texture_mesh.indices.size() == 0) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture mesh has no faces.";
return false;
}
if (texture.empty()) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture is empty.";
return false;
}
if (texture.channels() < 3) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture must have at least 3 channels, got " << texture.channels();
return false;
}
if (texture_mesh_uv_coords.size() != texture_mesh.indices.size()) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: uv_coords size is not equal to texture mesh faces size.";
return false;
}
for (std::size_t fid = 0; fid < texture_mesh.indices.size(); ++fid) {
if (texture_mesh_uv_coords[fid].size() != 3) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: uv_coords of single face size is not equal to 3.";
return false;
}
}
color_mesh = texture_mesh;
using Clock = std::chrono::high_resolution_clock;
const auto t_total_start = Clock::now();
auto t_step = t_total_start;
auto lap = [&](const char* step_name) {
auto now = Clock::now();
double ms = std::chrono::duration<double, std::milli>(now - t_step).count();
BOOST_LOG_TRIVIAL(debug) << "[timing] " << step_name << ": " << ms << "ms"
<< " faces=" << color_mesh.facets_count();
t_step = now;
};
report(5, "Oversampling");
if (cancelled()) {
return false;
}
// Step 1: Oversampling (subdivision while propagating UVs)
// Convert external vector<vector<Vec2f>> to internal vector<array<Vec2f,3>> to eliminate inner-level heap allocations
std::vector<FaceUVArray> color_mesh_uv_coords(texture_mesh_uv_coords.size());
for (std::size_t i = 0; i < texture_mesh_uv_coords.size(); ++i) {
color_mesh_uv_coords[i] = {texture_mesh_uv_coords[i][0], texture_mesh_uv_coords[i][1], texture_mesh_uv_coords[i][2]};
}
{
// Estimate total iterations and map each iteration's sub-progress to the [5, 25] range
size_t estimated_iters = 0;
if (settings.oversampling_iters > 0) {
estimated_iters = settings.oversampling_iters;
} else {
size_t fc = color_mesh.facets_count();
while (fc < settings.oversampling_min_face_count) {
fc *= 4;
++estimated_iters;
}
if (estimated_iters == 0) {
estimated_iters = 1;
}
}
auto make_iter_progress = [&](size_t iter) {
return [&, iter, estimated_iters](int pct) {
int iter_start = static_cast<int>(iter * 100 / estimated_iters);
int iter_end = static_cast<int>((iter + 1) * 100 / estimated_iters);
int sub_pct = iter_start + pct * (iter_end - iter_start) / 100;
sub_report(sub_pct, 5, 25, "Oversampling");
};
};
if (settings.oversampling_iters > 0) {
for (size_t i = 0; i < settings.oversampling_iters && color_mesh.facets_count() * 4.0 < settings.oversampling_max_face_count; ++i) {
if (cancelled()) return false;
linear_subdivision(color_mesh, color_mesh_uv_coords, make_iter_progress(i));
}
} else {
size_t iter = 0;
while (color_mesh.facets_count() < settings.oversampling_min_face_count) {
if (cancelled()) return false;
linear_subdivision(color_mesh, color_mesh_uv_coords, make_iter_progress(iter++));
}
}
}
lap("Oversampling");
face_colors.resize(color_mesh.indices.size());
report(25, "Computing face colors");
if (cancelled()) {
return false;
}
// Step 2: Compute each face's color (7-point Gaussian quadrature + bilinear interpolation sampling)
{
std::atomic<size_t> done_faces{0};
std::atomic<bool> cancel_requested{false};
const size_t total_faces = color_mesh.indices.size();
const size_t report_interval = std::max<size_t>(total_faces / 20, 1);
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total_faces), [&](const tbb::blocked_range<size_t>& range) {
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
if (cancel_requested.load(std::memory_order_relaxed)) return;
face_colors[fid] = sample_face_color(color_mesh_uv_coords[fid], texture);
size_t cnt = done_faces.fetch_add(1, std::memory_order_relaxed) + 1;
if (cnt % report_interval == 0) {
if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; }
sub_report(static_cast<int>(cnt * 100 / total_faces), 25, 40, "Computing face colors");
}
}
});
if (cancel_requested.load() || cancelled()) return false;
}
lap("Computing face colors");
#ifdef OUTPUT_TEST_RESULT
SaveToOFF("texture_to_color_0_initialize.off", color_mesh, face_colors);
#endif
report(40, "Repairing mesh");
if (cancelled()) {
return false;
}
// Sub-stage timing helper for the "Repairing mesh" outer lap. Logs each
// sub-phase under a [timing][Repairing mesh] prefix so that regressions in
// mesh inspection, RepairMesh, AABB resampling, etc. can be attributed
// to a specific sub-stage without changing the outer lap structure.
auto sub_lap = [&](const char* sub_name, Clock::time_point t0) {
double ms = std::chrono::duration<double, std::milli>(Clock::now() - t0).count();
BOOST_LOG_TRIVIAL(debug) << "[timing][Repairing mesh] " << sub_name << ": " << ms << "ms";
};
// Step 3: Repair mesh
// Many textured models have non-manifold, non-closed, or other issues that need to be fixed beforehand
auto resample_repaired_mesh = [&](TriMesh&& repaired_mesh) -> bool {
// AABBTreeIndirect references vertices/faces externally, so snapshot the
// pre-repair geometry by moving them out of color_mesh before it gets
// overwritten with the repaired mesh below. std::move on std::vector is
// O(1) (pointer adoption), no element copy.
const auto t_aabb = Clock::now();
TriVertices old_vertices = std::move(color_mesh.vertices);
TriFaces old_indices = std::move(color_mesh.indices);
auto before_repair_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices);
sub_lap("resample.aabb_build", t_aabb);
color_mesh = std::move(repaired_mesh);
const auto t_is_closed = Clock::now();
if (is_closed(color_mesh)) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repaired mesh is closed.";
} else {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repaired mesh is open.";
}
sub_lap("resample.is_closed", t_is_closed);
// New faces after repair inherit old face colors via centroid nearest-neighbor lookup.
// Since the mesh barely changes after repair, resampling via centroid nearest-neighbor is sufficient.
const auto t_resample = Clock::now();
std::vector<RGB> new_face_colors(color_mesh.facets_count());
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, color_mesh.facets_count()), [&](const tbb::blocked_range<size_t>& range) {
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
const auto& face = color_mesh.indices[fid];
Vec3f center = (color_mesh.vertices[face[0]] + color_mesh.vertices[face[1]] + color_mesh.vertices[face[2]]) / 3.0f;
size_t hit_idx = 0;
Vec3f closest;
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
old_vertices, old_indices, before_repair_tree, center, hit_idx, closest);
new_face_colors[fid] = face_colors[hit_idx];
}
});
face_colors = std::move(new_face_colors);
sub_lap("resample.parallel_nearest", t_resample);
return true;
};
auto repair_and_resample_mesh = [&]() -> bool {
std::shared_ptr<TriMesh> repaired_mesh;
const auto t_repair = Clock::now();
bool success = RepairMesh(color_mesh, repaired_mesh);
sub_lap("RepairMesh", t_repair);
if (success == false) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repair mesh failed.";
return false;
}
if (cancelled()) return false;
return resample_repaired_mesh(std::move(*repaired_mesh));
};
{
const auto t_stats = Clock::now();
TriangleMesh stats_mesh(static_cast<const indexed_triangle_set&>(color_mesh));
const auto& stats = stats_mesh.stats();
sub_lap("stats_check", t_stats);
// Orca's TriangleMeshStats defines manifold() as open_edges == 0 and does not track
// non-manifold edges/vertices separately, so BBS's "!manifold() || has_open_edges()"
// collapses to this single test and the extra counters drop out of the log.
if (!stats.manifold()) {
BOOST_LOG_TRIVIAL(info) << "TextureToColor: mesh has non-manifold geometry or open boundaries, open_edges="
<< stats.open_edges;
if (settings.mesh_repair_decision == MeshRepairDecision::Ask) {
if (settings.mesh_repair_decision_required)
*settings.mesh_repair_decision_required = true;
return false;
}
if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) {
indexed_triangle_set repaired_its;
std::string repair_error;
const auto t_win3d = Clock::now();
bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback(static_cast<const indexed_triangle_set&>(color_mesh), repaired_its,
[&](const char* message, unsigned percent) {
sub_report(static_cast<int>(percent), 40, 60, message ? message : "Repairing mesh");
},
[&]() { return cancelled(); }, &repair_error);
sub_lap("windows_3d_repair", t_win3d);
if (repaired) {
if (cancelled()) return false;
BOOST_LOG_TRIVIAL(info) << "TextureToColor: Windows 3D mesh repair finished.";
if (!resample_repaired_mesh(TriMesh(std::move(repaired_its))))
return false;
} else {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: Windows 3D mesh repair failed: " << repair_error;
}
} else {
BOOST_LOG_TRIVIAL(info) << "TextureToColor: importing mesh without Windows 3D repair.";
}
}
}
const auto t_halfedge = Clock::now();
const bool halfedge_ok = cgalutils::is_mesh_halfedge_compatible(color_mesh);
sub_lap("is_mesh_halfedge_compatible", t_halfedge);
if (!halfedge_ok && repair_and_resample_mesh() == false) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repair and resample mesh failed.";
return false;
}
lap("Repairing mesh");
#ifdef OUTPUT_TEST_RESULT
SaveToOFF("texture_to_color_1_repair.off", color_mesh, face_colors);
#endif
report(65, "Color clustering");
if (cancelled()) {
return false;
}
// Step 5: Color clustering
std::vector<RGB> cluster_centers;
std::vector<RGB> clustered_face_colors = face_colors;
std::vector<std::size_t> clustered_face_labels(face_colors.size());
const bool adaptive_cluster = settings.target_colors_num == 0;
// Compute cluster centers
if (adaptive_cluster) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: use cluster adaptive method.";
ClusterParameters para;
para.max_color_distance = settings.max_color_distance;
para.max_cluster_k = settings.max_cluster_k;
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
cluster_centers = cluster_adaptive(face_colors, para);
if (cancelled()) return false;
} else {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: use cluster k-means method.";
ClusterParameters para;
para.cluster_k = settings.target_colors_num;
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
cluster_centers = cluster_k_means(face_colors, para);
if (cancelled()) return false;
}
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: the k is " << cluster_centers.size() << ".";
if (cluster_centers.empty()) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: no cluster center generated.";
return false;
}
const std::set<RGB> unique_cluster_centers(cluster_centers.begin(), cluster_centers.end());
if (unique_cluster_centers.size() != cluster_centers.size()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cluster centers contain duplicated RGB values, unique exported colors may be fewer than centers.";
}
report(70, "Assigning cluster labels");
if (cancelled()) {
return false;
}
// Assign each face's color to the nearest cluster center
constexpr bool use_simple_cluster = true; // Complex algorithm is still being optimized; use simple assignment for now
if (use_simple_cluster) {
std::atomic<size_t> done_cluster{0};
std::atomic<bool> cancel_requested{false};
const size_t total_cluster = color_mesh.indices.size();
const size_t cluster_interval = std::max<size_t>(total_cluster / 20, 1);
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total_cluster), [&](const tbb::blocked_range<size_t>& range) {
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
if (cancel_requested.load(std::memory_order_relaxed)) return;
auto& face_color = face_colors[fid];
auto nearest_color_id = std::numeric_limits<std::size_t>::max();
bool success = calc_nearest_color_id(cluster_centers, face_color, nearest_color_id);
if (success == false) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: calc nearest color id failed.";
continue;
}
clustered_face_labels[fid] = nearest_color_id;
clustered_face_colors[fid] = cluster_centers[nearest_color_id];
size_t cnt = done_cluster.fetch_add(1, std::memory_order_relaxed) + 1;
if (cnt % cluster_interval == 0) {
if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; }
sub_report(static_cast<int>(cnt * 100 / total_cluster), 70, 85, "Assigning cluster labels");
}
}
});
if (cancel_requested.load() || cancelled()) return false;
} else {
bool success = mesh_cluster(color_mesh, cluster_centers, clustered_face_colors, clustered_face_labels);
if (success == false) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: mesh cluster failed.";
return false;
}
}
if (adaptive_cluster) {
if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment")) {
return false;
}
} else {
ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment");
}
lap("Color clustering & labeling");
#ifdef OUTPUT_TEST_RESULT
for (std::size_t i = 0; i < clustered_face_colors.size(); ++i) {
clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]];
}
SaveToOFF("texture_to_color_3_cluster.off", color_mesh, clustered_face_colors);
#endif
report(85, "Smoothing colors");
if (cancelled()) {
return false;
}
// Step 6: Post-process colors
SmoothParameters smooth_parameters;
smooth_parameters.smooth_weight = settings.smooth_weight;
if (!smooth_region(color_mesh, clustered_face_labels, smooth_parameters)) {
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: smooth region failed.";
return false;
}
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: smooth region success.";
if (adaptive_cluster) {
if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing")) {
return false;
}
} else {
ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing");
}
report(95, "Updating face colors");
if (cancelled()) {
return false;
}
for (std::size_t i = 0; i < clustered_face_colors.size(); ++i) {
clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]];
}
const std::set<RGB> unique_exported_colors(clustered_face_colors.begin(), clustered_face_colors.end());
if (unique_exported_colors.size() < cluster_centers.size()) {
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: final exported unique colors (" << unique_exported_colors.size()
<< ") are fewer than cluster centers (" << cluster_centers.size()
<< "), likely due to duplicate centers or unsatisfied seed assignment.";
}
#ifdef OUTPUT_TEST_RESULT
SaveToOFF("texture_to_color_4_smooth.off", color_mesh, clustered_face_colors);
#endif
face_colors = std::move(clustered_face_colors);
lap("Smoothing colors");
double total_ms = std::chrono::duration<double, std::milli>(Clock::now() - t_total_start).count();
BOOST_LOG_TRIVIAL(debug) << "[timing] TextureToColor total: " << total_ms << "ms"
<< " faces=" << color_mesh.facets_count();
report(100, "Completed");
return true;
}
} // namespace tex2color
} // namespace Slic3r
@@ -0,0 +1,65 @@
#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);
} // 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
+14
View File
@@ -1983,6 +1983,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)
+3
View File
@@ -369,6 +369,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);
+10
View File
@@ -353,6 +353,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
+943
View File
@@ -0,0 +1,943 @@
#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("#00AE42");
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);
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));
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("#909090")));
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("#909090")));
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("#D32F2F")));
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->SetBackgroundColor(StateColor::darkModeColorFor(*wxWHITE));
m_btn_cancel->SetBorderColor(StateColor::darkModeColorFor(wxColour("#CECECE")));
m_btn_cancel->SetTextColor(StateColor::darkModeColorFor(wxColour("#262E30")));
m_btn_cancel->SetMinSize(wxSize(FromDIP(55), FromDIP(24)));
m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
m_btn_ok = new Button(this, _L("OK"));
m_btn_ok->SetBackgroundColor(StateColor(
std::make_pair(wxColour("#C2C2C2"), (int) StateColor::Disabled),
std::make_pair(wxColour("#00AE42"), (int) StateColor::Normal)));
m_btn_ok->SetBorderColor(StateColor(
std::make_pair(wxColour("#C2C2C2"), (int) StateColor::Disabled),
std::make_pair(wxColour("#00AE42"), (int) StateColor::Normal)));
m_btn_ok->SetTextColor(StateColor(
std::make_pair(*wxWHITE, (int) StateColor::Disabled),
std::make_pair(*wxWHITE, (int) StateColor::Normal)));
m_btn_ok->SetMinSize(wxSize(FromDIP(55), FromDIP(24)));
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();
CenterOnParent();
}
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/recenter when the warning panel actually toggled from hidden
// to shown. While already visible, switching modes must not re-Fit/recenter
// the dialog, which would make it jump on every card switch.
if (!was_shown) {
Fit();
CenterOnParent();
}
}
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_
+49 -5
View File
@@ -577,17 +577,30 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
// BBS
static const char* keys[] = { "support_filament", "support_interface_filament"};
// A per-role filament override must name a real, physical filament. Out-of-range values are
// stale; a mixed-color slot is virtual and cannot be driven directly by a role override, so
// both are reset to 0 ("inherit the object's filament"). The object's own extruder assignment
// is what legitimately carries a mixed slot. Orca splits BBS's wall/solid_infill roles into
// six keys, so all of them are checked here.
static const char* keys[] = { "support_filament", "support_interface_filament",
"outer_wall_filament_id", "inner_wall_filament_id",
"sparse_infill_filament_id", "internal_solid_filament_id",
"top_surface_filament_id", "bottom_surface_filament_id" };
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
std::string key = std::string(keys[i]);
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (opt != nullptr) {
if (opt->getInt() > filament_cnt) {
int val = opt->getInt();
bool out_of_range = val > filament_cnt;
bool is_mixed = (val > 0 && val <= filament_cnt &&
wxGetApp().preset_bundle->is_mixed_filament(val - 1));
if (out_of_range || is_mixed) {
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 (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);
@@ -595,6 +608,37 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
}
// 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;
}
}
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 &&
config->get_abs_value("seam_slope_start_height") >= layer_height) {
const wxString msg_text = _(L("seam_slope_start_height need to be smaller than layer_height.\nReset to 0."));
+12
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)
{
@@ -2984,6 +2989,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);
@@ -3010,6 +3018,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);
@@ -10570,6 +10579,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;
+1
View File
@@ -391,6 +391,7 @@ class GLCanvas3D
PrimeTowerOutside,
NozzleFilamentIncompatible,
MixtureFilamentIncompatible,
SingleExtruderMixedFilament,
FlushingVolumeZero
};
@@ -731,6 +731,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 +757,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);
// As above: a mixed-color slot can index past the physical colour list.
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());
+644
View File
@@ -0,0 +1,644 @@
#include "GradientCurveEditor.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
#include "I18N.hpp"
#include "Widgets/StateColor.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <wx/dcbuffer.h>
#include <wx/dcclient.h>
#include <wx/dcgraph.h>
#include <wx/settings.h>
namespace Slic3r {
namespace GUI {
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
namespace {
// Layout (Figma "Property 1=Default", 214.06 x 179.63 px reference).
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels.
constexpr double kPlotLeftRatio = 0.0316;
constexpr double kPlotRightRatio = 0.6766;
constexpr double kPlotTopRatio = 0.1529;
constexpr double kPlotBottomRatio = 0.8474;
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
// Hit / stroke (DIP).
constexpr int kHitRadius = 6;
constexpr int kCurveHitRadius = 5;
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
constexpr int kStrokeUnselected = 2;
constexpr int kStrokeSelected = 4;
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention)
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
// Light-mode design tokens from Figma. Resolved through StateColor::darkModeColorFor()
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
// #818183, #262E30 -> #EFEFF0, *wxWHITE -> #2D2D31). Don't read these directly in paint;
// always go through the resolved locals declared at the top of on_paint().
const wxColour kGridColor (238, 238, 238); // #EEEEEE grey 300
const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
// LAB (DeltaE76) threshold for "curve color is too close to the background". Below this
// we paint a subtle axis-color outline so the curve doesn't visually vanish; above this
// we draw the curve plain. ~15 is "perceptible but still close", looser than the strict
// 5.0 used by FlushPredict::is_similar_color but loose enough that a pastel pink on white
// or a charcoal on #2B2B2B still triggers an outline.
constexpr float kBgSimilarThreshold = 15.0f;
constexpr int kOutlineExtraDip = 2;
} // namespace
GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
const wxColour& color_low,
const wxColour& color_high)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
, m_color_low(color_low)
, m_color_high(color_high)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetBackgroundColour(wxGetApp().get_window_default_clr());
// Wide enough so the X-axis "Material Ratio" label fits past the arrow tip without overlap.
// 260 (was 240): adds room for the "Material Ratio" label that gets shifted right by the
// longer axis arrow; the hosting MixedFilamentDialog grows to 470 DIP to accommodate.
SetMinSize(FromDIP(wxSize(260, 200)));
reset_to_linear(0.10, 0.90);
Bind(wxEVT_PAINT, &GradientCurveEditor::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &GradientCurveEditor::on_left_down, this);
Bind(wxEVT_LEFT_UP, &GradientCurveEditor::on_left_up, this);
Bind(wxEVT_RIGHT_DOWN, &GradientCurveEditor::on_right_down, this);
Bind(wxEVT_MOTION, &GradientCurveEditor::on_motion, this);
Bind(wxEVT_LEAVE_WINDOW,&GradientCurveEditor::on_leave, this);
Bind(wxEVT_SIZE, &GradientCurveEditor::on_size, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
m_drag_mode = DragMode::None;
m_drag_idx = -1;
m_dragged_moved = false;
});
}
void GradientCurveEditor::set_points(const PointList& pts)
{
m_points = pts;
normalize_points();
Refresh();
}
void GradientCurveEditor::set_colors(const wxColour& color_low, const wxColour& color_high)
{
m_color_low = color_low;
m_color_high = color_high;
Refresh();
}
void GradientCurveEditor::set_selected_curve(int curve_idx)
{
const int new_sel = (curve_idx == 0) ? 0 : 1;
if (m_selected_curve == new_sel) return;
m_selected_curve = new_sel;
Refresh();
}
void GradientCurveEditor::reset_to_linear(double y0, double y1)
{
auto clamp_y = [](double v) {
return std::max(kGradientMinRatio, std::min(kGradientMaxRatio, v));
};
m_points.clear();
GradientAnchor a0; a0.x = 0.0; a0.y = clamp_y(y0);
GradientAnchor a1; a1.x = 1.0; a1.y = clamp_y(y1);
m_points.push_back(a0);
m_points.push_back(a1);
m_selected_curve = 0;
Refresh();
emit_changed();
}
void GradientCurveEditor::reverse()
{
// Mirror y around 0.5. Tangents are slopes dy/dx so they flip sign to keep the
// local shape consistent across the mirror; NaN tangents remain "use PCHIP default".
for (auto& p : m_points) {
p.y = 1.0 - p.y;
if (std::isfinite(p.m_in)) p.m_in = -p.m_in;
if (std::isfinite(p.m_out)) p.m_out = -p.m_out;
}
Refresh();
emit_changed();
}
void GradientCurveEditor::normalize_points()
{
if (m_points.empty()) {
GradientAnchor a0; a0.x = 0.0; a0.y = kGradientMinRatio;
GradientAnchor a1; a1.x = 1.0; a1.y = kGradientMaxRatio;
m_points.push_back(a0);
m_points.push_back(a1);
return;
}
for (auto& p : m_points) {
p.x = std::max(0.0, std::min(1.0, p.x));
p.y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, p.y));
}
std::sort(m_points.begin(), m_points.end(),
[](const GradientAnchor& a, const GradientAnchor& b) {
return a.x < b.x;
});
if (m_points.size() < 2) {
GradientAnchor tail; tail.x = 1.0; tail.y = m_points.front().y;
m_points.push_back(tail);
}
m_points.front().x = 0.0;
m_points.back().x = 1.0;
}
void GradientCurveEditor::emit_changed()
{
wxCommandEvent evt(wxEVT_GRADIENT_CURVE_CHANGED, GetId());
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
wxRect GradientCurveEditor::plot_rect() const
{
const wxSize sz = GetClientSize();
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
// Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at
// the top-left so the "100%" labels on the bottom/right still align with the plot edges.
const int side = std::max(1, std::min(x2 - x, y2 - y));
return wxRect(x, y, side, side);
}
wxPoint GradientCurveEditor::data_to_px(double x, double y) const
{
const wxRect r = plot_rect();
const int px = r.x + static_cast<int>(std::lround(x * r.width));
// y axis is inverted: y=1 should sit at the top.
const int py = r.y + static_cast<int>(std::lround((1.0 - y) * r.height));
return wxPoint(px, py);
}
void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const
{
const wxRect r = plot_rect();
const double w = std::max(1, r.width);
const double h = std::max(1, r.height);
x = std::max(0.0, std::min(1.0, (px - r.x) / w));
y = std::max(0.0, std::min(1.0, 1.0 - (py - r.y) / h));
}
double GradientCurveEditor::sample_curve_y(double x) const
{
GradientCurve gc;
gc.points = m_points;
return sample_gradient_curve(gc, x);
}
int GradientCurveEditor::hit_test(int px, int py) const
{
const int tol = FromDIP(kHitRadius);
int best_idx = -1;
int best_d2 = tol * tol;
for (size_t i = 0; i < m_points.size(); ++i) {
// Anchor visual y is curve-specific: component 1's anchor sits at (x, 1 - stored_y).
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
const wxPoint p = data_to_px(m_points[i].x, vy);
const int dx = px - p.x;
const int dy = py - p.y;
const int d2 = dx * dx + dy * dy;
if (d2 <= best_d2) {
best_idx = static_cast<int>(i);
best_d2 = d2;
}
}
return best_idx;
}
int GradientCurveEditor::hit_test_curve(int px, int py, int* seg_out) const
{
if (seg_out) *seg_out = -1;
if (m_points.size() < 2) return -1;
const int tol = FromDIP(kCurveHitRadius);
const int tol2 = tol * tol;
auto dist2_to_seg = [&](int ax, int ay, int bx, int by) -> int {
const double dx = bx - ax;
const double dy = by - ay;
const double l2 = dx * dx + dy * dy;
if (l2 == 0.0) {
const double ddx = px - ax;
const double ddy = py - ay;
return static_cast<int>(ddx * ddx + ddy * ddy);
}
double t = ((px - ax) * dx + (py - ay) * dy) / l2;
t = std::max(0.0, std::min(1.0, t));
const double ex = ax + t * dx;
const double ey = ay + t * dy;
const double ddx = px - ex;
const double ddy = py - ey;
return static_cast<int>(ddx * ddx + ddy * ddy);
};
// Hit-test against the same dense Hermite polyline that on_paint draws, so the
// clickable line follows the visual curve exactly (no offset on the bent parts).
// When a hit is found, also report the index of the left anchor of the data-space
// segment that covers cursor x; needed by the segment-bend interaction.
const wxRect rc = plot_rect();
const int samples = std::max(128, rc.width * 2);
auto seg_for_x = [&](double cursor_x) -> int {
for (size_t i = 1; i < m_points.size(); ++i) {
if (cursor_x <= m_points[i].x)
return static_cast<int>(i - 1);
}
return static_cast<int>(m_points.size() - 2);
};
auto curve_hit = [&](int curve_idx) -> bool {
wxPoint prev;
for (int s = 0; s <= samples; ++s) {
const double x = double(s) / samples;
const double y0 = sample_curve_y(x);
const double vy = to_visual_y(curve_idx, y0);
const wxPoint cur = data_to_px(x, vy);
if (s > 0 && dist2_to_seg(prev.x, prev.y, cur.x, cur.y) <= tol2)
return true;
prev = cur;
}
return false;
};
// Prefer the selected curve so overlapping segments don't unintentionally steal focus.
if (curve_hit(m_selected_curve)) {
if (seg_out) {
double nx = 0, dummy = 0;
px_to_data(px, py, nx, dummy);
*seg_out = seg_for_x(nx);
}
return m_selected_curve;
}
const int other = 1 - m_selected_curve;
if (curve_hit(other)) {
if (seg_out) {
double nx = 0, dummy = 0;
px_to_data(px, py, nx, dummy);
*seg_out = seg_for_x(nx);
}
return other;
}
return -1;
}
void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/)
{
// Resolve theme colors every paint so dark-mode toggles (no re-construction) take
// effect without an explicit listener. Window bg is read from GUI_App, not
// GetBackgroundColour(), since the latter is snapshotted at construction time.
const wxColour bg = wxGetApp().get_window_default_clr();
const wxColour grid_color = StateColor::darkModeColorFor(kGridColor);
const wxColour axis_color = StateColor::darkModeColorFor(kAxisColor);
const wxColour label_muted = StateColor::darkModeColorFor(kLabelMuted);
const wxColour label_strong = StateColor::darkModeColorFor(kLabelStrong);
const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE);
wxAutoBufferedPaintDC raw_dc(this);
raw_dc.SetBackground(wxBrush(bg));
raw_dc.Clear();
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered
// DC is the actual back buffer that gets blitted to the window.
wxGCDC dc(raw_dc);
const wxRect rc = plot_rect();
if (rc.width <= 0 || rc.height <= 0)
return;
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
dc.SetPen(wxPen(grid_color, 1));
for (int i = 0; i <= kGridDivisions; ++i) {
const int x = rc.x + rc.width * i / kGridDivisions;
const int y = rc.y + rc.height * i / kGridDivisions;
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
}
// Set the label font first so text width measurements drive arrow / label placement.
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
dc.SetFont(label_font);
const wxString axis_y_title = _L("Material Ratio");
const wxString axis_x_title = _L("Model Height");
const wxString pct_text = wxT("100%");
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
wxFont strong_font = label_font;
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
dc.SetFont(strong_font);
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
dc.SetFont(label_font);
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
const int arrow_half = FromDIP(kAxisArrowHalf);
const int arrow_len = FromDIP(kAxisArrowLen);
const wxSize sz = GetClientSize();
dc.SetPen(wxPen(axis_color, kStrokeAxis));
dc.SetBrush(wxBrush(axis_color));
// Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom.
const int y_axis_x = rc.x;
const int y_title_pct_gap = FromDIP(1);
const int y_title_bottom_pad = FromDIP(2);
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
const int y_arrow_tip_y = y_title_y;
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
{
wxPoint tri[3] = {
wxPoint(y_axis_x, y_arrow_tip_y),
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
};
dc.DrawPolygon(3, tri);
}
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing
// "Material Ratio" label still fits inside the canvas without overlapping the arrow.
const int x_axis_y = rc.y + rc.height;
const int x_label_gap = FromDIP(4);
const int x_edge_pad = FromDIP(6);
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10);
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len,
std::min(x_arrow_ideal, x_arrow_max));
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
const int x_title_x = x_arrow_tip_x + x_label_gap;
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
{
wxPoint tri[3] = {
wxPoint(x_arrow_tip_x, x_axis_y),
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
};
dc.DrawPolygon(3, tri);
}
// Labels.
// "Model Height" and "100%" share the same left x; the gap is larger than the
// axis-arrow half-base so the text never visually touches the Y-axis arrow.
const int label_left_x = y_axis_x + FromDIP(10);
dc.SetTextForeground(label_muted);
dc.DrawText(axis_y_title, label_left_x, y_title_y);
dc.SetFont(strong_font);
dc.SetTextForeground(label_strong);
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
// Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the
// X-axis arrow tip (placement was already clamped above to leave room).
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
dc.SetFont(label_font);
dc.SetTextForeground(label_muted);
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
if (m_points.size() < 2)
return;
auto color_for_curve = [&](int curve_idx) -> wxColour {
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
// Lift alpha so the curve stays visible while still hinting at transparency.
if (c.Alpha() == 0)
c.Set(c.Red(), c.Green(), c.Blue(), 150);
return c;
};
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint> {
const int samples = std::max(128, rc.width * 2);
std::vector<wxPoint> poly;
poly.reserve(samples + 1);
for (int s = 0; s <= samples; ++s) {
const double x = double(s) / samples;
const double y0 = sample_curve_y(x);
const double vy = to_visual_y(curve_idx, y0);
poly.push_back(data_to_px(x, vy));
}
return poly;
};
auto draw_polyline = [&](const std::vector<wxPoint>& poly, const wxColour& col, int stroke_dip) {
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
dc.DrawLines(static_cast<int>(poly.size()), poly.data());
};
// Outline only when the curve color is perceptually close to the background; otherwise
// the plain filament color reads fine and the extra stroke would look heavy.
// Outline tone is intentionally softer than axis_color so it disambiguates the curve
// from the bg without competing with the structural axis/grid: light mode uses a pale
// grey, dark mode uses a slightly-above-bg grey (gDarkColors has no entry for these).
const wxColour outline_color = wxGetApp().dark_mode()
? wxColour(90, 90, 94) // > bg #2B2B2B, < axis #818183
: wxColour(200, 200, 200); // > grid #EEEEEE, < axis #6B6B6B
auto needs_outline = [&](const wxColour& c) {
return calc_color_distance(c, bg) < kBgSimilarThreshold;
};
auto draw_one = [&](int curve_idx, int stroke_dip) {
const auto poly = build_polyline(curve_idx);
const wxColour col = color_for_curve(curve_idx);
if (needs_outline(col))
draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip);
draw_polyline(poly, col, stroke_dip);
};
// Draw unselected first so the selected curve sits on top.
const int other = 1 - m_selected_curve;
draw_one(other, kStrokeUnselected);
draw_one(m_selected_curve, kStrokeSelected);
// Control points (selected curve only): hollow circle with axis-color border, theme-aware fill.
const int r = FromDIP(kPointRadius);
dc.SetPen(wxPen(axis_color, 1));
dc.SetBrush(wxBrush(point_fill));
for (size_t i = 0; i < m_points.size(); ++i) {
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
const wxPoint p = data_to_px(m_points[i].x, vy);
dc.DrawCircle(p.x, p.y, r);
}
}
void GradientCurveEditor::on_left_down(wxMouseEvent& evt)
{
const wxPoint pos = evt.GetPosition();
m_dragged_moved = false;
// 1) Anchor on the selected curve takes precedence over everything else.
// Dragging an anchor resets its tangent overrides so the surrounding curve
// returns to PCHIP-default shape (matches user expectation that pulling an
// anchor "straightens out" the local mess).
const int idx = hit_test(pos.x, pos.y);
if (idx >= 0) {
m_drag_mode = DragMode::Anchor;
m_drag_idx = idx;
// Only emit a change event when clearing the tangents actually mutates
// the curve. A plain click on an already-default anchor must not trigger
// re-slicing through the changed-event listener.
const bool had_tangent = std::isfinite(m_points[idx].m_in)
|| std::isfinite(m_points[idx].m_out);
m_points[idx].m_in = std::numeric_limits<double>::quiet_NaN();
m_points[idx].m_out = std::numeric_limits<double>::quiet_NaN();
if (!HasCapture())
CaptureMouse();
Refresh();
if (had_tangent)
emit_changed();
return;
}
// 2) Line-body hit. Determine which curve and which segment.
int seg = -1;
const int curve_hit = hit_test_curve(pos.x, pos.y, &seg);
if (curve_hit < 0) {
m_drag_mode = DragMode::None;
evt.Skip();
return;
}
// 3) Non-selected curve hit -> switch selection only, no drag arming.
if (curve_hit != m_selected_curve) {
m_selected_curve = curve_hit;
m_drag_mode = DragMode::None;
Refresh();
evt.Skip();
return;
}
// 4) Selected curve line body hit -> insert a new anchor at cursor x (snapped
// to the current smooth curve so the initial click is visually invisible)
// and immediately enter Anchor drag mode. PS Curves style: the drag-bend
// interaction has no separate "bend without anchor" mode; pressing and
// dragging on the line is equivalent to clicking to add then dragging the
// fresh anchor. Trades the previous (failed) "no anchor on drag" promise
// for genuine cursor tracking, since a single cubic between two existing
// anchors mathematically cannot put its peak under an off-center cursor.
double nx = 0, dummy = 0;
px_to_data(pos.x, pos.y, nx, dummy);
if (nx <= 0.0 || nx >= 1.0 || seg < 0) {
m_drag_mode = DragMode::None;
evt.Skip();
return;
}
GradientAnchor a;
a.x = nx;
a.y = sample_curve_y(nx);
const size_t insert_idx = static_cast<size_t>(seg) + 1;
m_points.insert(m_points.begin() + insert_idx, a);
m_drag_mode = DragMode::Anchor;
m_drag_idx = static_cast<int>(insert_idx);
if (!HasCapture())
CaptureMouse();
Refresh();
emit_changed();
}
void GradientCurveEditor::on_left_up(wxMouseEvent& evt)
{
if (HasCapture())
ReleaseMouse();
// Anchor mode (either an existing anchor or one freshly inserted by on_left_down)
// already fired emit_changed on mouse_down; only fire again here if the user
// actually dragged so the slicer doesn't re-run on a pure click.
if (m_drag_mode == DragMode::Anchor && m_dragged_moved)
emit_changed();
m_drag_mode = DragMode::None;
m_drag_idx = -1;
m_dragged_moved = false;
(void)evt;
}
void GradientCurveEditor::on_right_down(wxMouseEvent& evt)
{
const wxPoint pos = evt.GetPosition();
const int idx = hit_test(pos.x, pos.y);
if (idx > 0 && static_cast<size_t>(idx) + 1 < m_points.size()) {
// Interior anchor on the selected curve -> delete it. Endpoints stay locked.
m_points.erase(m_points.begin() + idx);
Refresh();
emit_changed();
return;
}
// Right-click on the non-selected curve switches selection (never deletes).
const int curve_hit = hit_test_curve(pos.x, pos.y);
if (curve_hit >= 0 && curve_hit != m_selected_curve) {
m_selected_curve = curve_hit;
Refresh();
return;
}
evt.Skip();
}
void GradientCurveEditor::on_motion(wxMouseEvent& evt)
{
if (!evt.LeftIsDown() || m_drag_mode != DragMode::Anchor) {
evt.Skip();
return;
}
if (static_cast<size_t>(m_drag_idx) >= m_points.size())
return;
const wxPoint pos = evt.GetPosition();
double nx = 0, vy = 0;
px_to_data(pos.x, pos.y, nx, vy);
auto& p = m_points[m_drag_idx];
const bool is_first = (m_drag_idx == 0);
const bool is_last = (static_cast<size_t>(m_drag_idx) + 1 == m_points.size());
// Endpoints stay locked at x=0 / x=1; interior anchors clamp into
// (left_neighbor.x, right_neighbor.x) so they can't cross or coincide.
if (!is_first && !is_last) {
const double xl = m_points[m_drag_idx - 1].x;
const double xr = m_points[m_drag_idx + 1].x;
const double eps = 1e-4;
nx = std::max(xl + eps, std::min(xr - eps, nx));
p.x = nx;
}
// y is constrained to the reserved blend band so neither component ever
// reaches 0% / 100%, matching the sampler's clamp.
p.y = std::max(kGradientMinRatio,
std::min(kGradientMaxRatio, to_stored_y(m_selected_curve, vy)));
m_dragged_moved = true;
Refresh();
}
void GradientCurveEditor::on_leave(wxMouseEvent& evt)
{
evt.Skip();
}
void GradientCurveEditor::on_size(wxSizeEvent& evt)
{
Refresh();
evt.Skip();
}
} // namespace GUI
} // namespace Slic3r
+115
View File
@@ -0,0 +1,115 @@
#ifndef slic3r_GradientCurveEditor_hpp_
#define slic3r_GradientCurveEditor_hpp_
#include <vector>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/panel.h>
#include "libslic3r/FilamentMixer.hpp"
namespace Slic3r {
namespace GUI {
// Photoshop-style curve editor for "Z progress -> first-component ratio" mapping.
// Curve evaluation uses cubic Hermite with PCHIP defaults plus optional per-anchor
// tangent overrides (m_in / m_out, NaN = use PCHIP default). The same evaluator
// (FilamentMixer::sample_gradient_curve) is shared with the slicing backend so what
// the editor renders matches the G-code output 1:1.
//
// Interaction model (PS Curves style):
// - Click or press-and-drag on the line body inserts a new anchor at the cursor x
// (snapped to the current smooth curve, NaN tangents) and starts dragging it.
// A pure click leaves an anchor sitting exactly on the previous curve shape; a
// drag moves the new anchor freely so the bump follows the cursor 1:1.
// - Dragging an existing anchor moves (x, y) and clears its m_in / m_out so the
// local curve returns to the PCHIP default shape around it.
// - Right-click on an interior anchor deletes it; endpoints stay locked.
class GradientCurveEditor : public wxPanel
{
public:
using PointList = std::vector<GradientAnchor>;
GradientCurveEditor(wxWindow* parent,
const wxColour& color_low = wxColour(217, 217, 217),
const wxColour& color_high = wxColour(217, 217, 217));
// Replace the entire point list. The widget enforces x in [0,1], y in [0,1],
// sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are
// preserved as-is (NaN entries continue to use PCHIP defaults).
void set_points(const PointList& pts);
const PointList& get_points() const { return m_points; }
void set_colors(const wxColour& color_low, const wxColour& color_high);
// Which curve currently responds to drag / add / delete and is drawn with the thick stroke.
// 0 = first component (color_low), 1 = second component (color_high). Storage layer is
// unaffected: m_points always represents component 0's ratio.
void set_selected_curve(int curve_idx);
int get_selected_curve() const { return m_selected_curve; }
// Reset to a two-point linear curve from y0 at t=0 to y1 at t=1.
// Clears all tangent overrides.
void reset_to_linear(double y0, double y1);
// Flip the curve top to bottom (all y -> 1 - y; tangents negated to mirror shape).
void reverse();
private:
enum class DragMode {
None, // nothing armed
Anchor, // dragging an anchor (either existing or just inserted from a line hit)
};
void normalize_points();
void emit_changed();
void on_paint(wxPaintEvent& evt);
void on_left_down(wxMouseEvent& evt);
void on_left_up(wxMouseEvent& evt);
void on_right_down(wxMouseEvent& evt);
void on_motion(wxMouseEvent& evt);
void on_leave(wxMouseEvent& evt);
void on_size(wxSizeEvent& evt);
// Coordinate mapping between data (x, y in [0,1]) and pixels in plot area.
wxRect plot_rect() const;
wxPoint data_to_px(double x, double y) const;
void px_to_data(int px, int py, double& x, double& y) const;
// Anchor hit test for the currently-selected curve (uses translated visual y).
int hit_test(int px, int py) const; // returns point index or -1
// Line-body hit test across both curves. Returns 0/1 for which curve was hit, -1 if none.
// Prefers the selected curve when both are within threshold. seg_out (when non-null)
// receives the left-anchor index of the segment that was hit on the returned curve;
// on_left_down uses it to know where in m_points to insert a freshly-added anchor.
int hit_test_curve(int px, int py, int* seg_out = nullptr) const;
// Sample the curve in stored space (component 0) at x.
double sample_curve_y(double x) const;
// Symmetric translation between visual y (what the user sees / clicks) and stored y
// (component 0's ratio in m_points).
static double to_stored_y(int curve_idx, double visual_y) {
return (curve_idx == 0) ? visual_y : (1.0 - visual_y);
}
static double to_visual_y(int curve_idx, double stored_y) {
return (curve_idx == 0) ? stored_y : (1.0 - stored_y);
}
PointList m_points;
wxColour m_color_low;
wxColour m_color_high;
int m_selected_curve = 0;
DragMode m_drag_mode = DragMode::None;
int m_drag_idx = -1; // valid when m_drag_mode == Anchor
bool m_dragged_moved = false;
};
// Custom event raised when the curve is edited (drag / add / remove / reset / reverse).
wxDECLARE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GradientCurveEditor_hpp_
+5
View File
@@ -2330,6 +2330,11 @@ bool MainFrame::get_enable_slice_status()
}
}
// A mixed filament whose components were deleted, or whose components disagree in type,
// cannot be resolved at slicing time. Block the slice until the user fixes it.
if (enable && m_plater->sidebar().has_broken_mixed_filament())
enable = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable;
return enable;
}
File diff suppressed because it is too large Load Diff
+176
View File
@@ -0,0 +1,176 @@
#ifndef slic3r_MixedFilamentDialog_hpp_
#define slic3r_MixedFilamentDialog_hpp_
#include <array>
#include <string>
#include <utility>
#include <vector>
#include <wx/bitmap.h>
#include <wx/panel.h>
#include <wx/tglbtn.h>
#include <wx/stattext.h>
#include "GUI_Utils.hpp"
#include "libslic3r/FilamentMixer.hpp"
class Button;
class CheckBox;
class ComboBox;
class wxMouseEvent;
class wxScrolledWindow;
class wxTextCtrl;
class wxWrapSizer;
namespace Slic3r {
namespace GUI {
class GradientCurveEditor;
class RatioLabelPanel;
struct MixedFilamentResult {
std::vector<unsigned int> components; // 1-based physical filament indices
std::vector<int> ratios; // percentages, sum = 100
bool gradient_enabled = false;
int gradient_direction = 0; // 0 = A→B, 1 = B→A (only for 2-color)
bool per_part_gradient = false; // valid only when gradient_enabled == true
// Optional Photoshop-style custom curve overriding the linear A→B gradient.
// Empty -> use linear (gradient_direction). Non-empty -> cubic Hermite over [0,1]^2
// with optional per-anchor tangent overrides (see GradientAnchor).
std::vector<GradientAnchor> gradient_curve;
};
class MixedFilamentDialog : public DPIDialog
{
public:
MixedFilamentDialog(wxWindow* parent,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_names,
const std::vector<std::string>& physical_types = {});
MixedFilamentDialog(wxWindow* parent,
const MixedFilamentResult& existing,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_names,
const std::vector<std::string>& physical_types = {});
MixedFilamentResult get_result() const { return m_result; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void build_ui();
wxBoxSizer* create_preview_panel();
wxBoxSizer* create_material_selection();
wxBoxSizer* create_ratio_slider();
wxBoxSizer* create_triangle_picker();
wxBoxSizer* create_gradient_section();
wxBoxSizer* create_recommendation_grid();
wxBoxSizer* create_button_panel();
void on_filament_changed();
void on_ratio_changed(int new_ratio_a);
void on_gradient_toggled();
void on_gradient_direction_changed();
void on_gradient_curve_changed();
void on_per_part_gradient_toggled();
void on_add_material();
void on_remove_material();
void on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b);
void on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c);
void apply_manual_ratio(size_t idx, int value);
void apply_dragged_triangle_ratio(int r0, int r1, int r2);
void reset_manual_ratio_state();
void refresh_ratio_labels();
void sync_triangle_weights_from_ratios();
void start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect);
void commit_ratio_editor(bool apply);
void commit_ratio_editor_from_background(wxMouseEvent& e);
void update_preview();
void update_ok_button_state();
void update_gradient_direction_items();
void update_component_count_ui();
// Picks dialog (width, height) based on current state so the gradient curve
// editor and the recommendation list stay visible at the same time.
wxSize compute_dialog_size() const;
void rebuild_all_combos();
void rebuild_recommendation_items();
void refresh_curve_editor_colors();
void paint_warning_panel(wxPaintEvent& evt);
wxBitmap make_swatch_bitmap(size_t idx);
// Helpers for component/ratio access
size_t num_components() const { return m_result.components.size(); }
unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; }
int ratio(size_t i) const { return (i < m_result.ratios.size()) ? m_result.ratios[i] : 0; }
wxColour comp_colour(size_t i) const;
MixedFilamentResult m_result;
bool m_edit_mode{false};
std::vector<std::string> m_physical_colors;
std::vector<std::string> m_physical_names;
std::vector<std::string> m_physical_types;
wxString m_type_mismatch_msg;
// Combo item index -> 1-based physical filament index (per combo)
std::vector<std::vector<unsigned int>> m_combo_to_physical;
// UI controls
wxPanel* m_preview_canvas{nullptr};
wxPanel* m_summary_panel{nullptr};
std::vector<ComboBox*> m_combo_filaments;
wxBoxSizer* m_material_rows_sizer{nullptr};
wxPanel* m_ratio_bar{nullptr};
wxPanel* m_triangle_panel{nullptr};
RatioLabelPanel* m_label_ratio_a{nullptr};
RatioLabelPanel* m_label_ratio_b{nullptr};
wxPanel* m_ratio_editor_panel{nullptr};
wxTextCtrl* m_ratio_editor{nullptr};
CheckBox* m_chk_gradient{nullptr};
wxStaticText* m_label_gradient{nullptr};
ComboBox* m_combo_gradient_dir{nullptr};
wxBoxSizer* m_gradient_sizer{nullptr};
GradientCurveEditor* m_curve_editor{nullptr};
wxBoxSizer* m_curve_sizer{nullptr};
CheckBox* m_chk_per_part_gradient{nullptr};
wxStaticText* m_label_per_part_gradient{nullptr};
wxBoxSizer* m_per_part_gradient_sizer{nullptr};
Button* m_btn_add_material{nullptr};
Button* m_btn_remove_material{nullptr};
Button* m_btn_ok{nullptr};
Button* m_btn_cancel{nullptr};
wxBoxSizer* m_warning_sizer{nullptr};
wxPanel* m_warning_panel{nullptr};
wxBoxSizer* m_ratio_sizer{nullptr};
wxBoxSizer* m_triangle_sizer{nullptr};
wxBoxSizer* m_right_sizer{nullptr};
wxScrolledWindow* m_recommendation_scroll{nullptr};
wxWrapSizer* m_recommendation_grid{nullptr};
// Cached preview bitmaps (loaded once at construction)
wxBitmap m_preview_bmp_two;
wxBitmap m_preview_bmp_three;
// Drag state
bool m_dragging{false};
std::vector<size_t> m_ratio_manual_order;
size_t m_ratio_editor_idx{0};
bool m_ratio_editor_committing{false};
wxWindow* m_ratio_editor_anchor{nullptr};
// Triangle picker drag point (barycentric weights)
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334};
// Cached triangle color bitmap (invalidated when colors or size change)
wxBitmap m_tri_cache_bmp;
wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2;
wxSize m_tri_cache_size;
std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_MixedFilamentDialog_hpp_
+2
View File
@@ -162,6 +162,8 @@ enum class NotificationType
//BBL: plugin install hint
BBLPluginInstallHint,
BBLFlushingVolumeZero,
// A mixed-color filament references a deleted component, or its components disagree in type.
BBLMixedFilamentBroken,
BBLPluginUpdateAvailable,
BBLPreviewOnlyMode,
BBLPrinterConfigUpdateAvailable,
+106
View File
@@ -6,6 +6,7 @@
#include <sstream>
#include <regex>
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include <future>
#include <glad/gl.h>
#include <boost/algorithm/string.hpp>
@@ -1675,6 +1676,25 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
plate_extruders.clear();
for (unsigned int e : expanded)
plate_extruders.push_back((int)(e + 1));
}
}
return plate_extruders;
}
@@ -1836,6 +1856,24 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
plate_extruders.clear();
for (unsigned int e : expanded)
plate_extruders.push_back((int)(e + 1));
}
}
return plate_extruders;
}
@@ -1889,6 +1927,25 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
plate_extruders.clear();
for (unsigned int e : expanded)
plate_extruders.push_back((int)(e + 1));
}
}
return plate_extruders;
}
@@ -1990,6 +2047,55 @@ bool PartPlate::check_tpu_printable_status(const DynamicPrintConfig & config, co
return true;
}
// A mixed-color filament alternates between its components constantly. On a single-nozzle
// printer every one of those switches is a full filament change plus a purge, so warn the
// user before they commit to it. Printers with more than one nozzle can keep the components
// loaded simultaneously and are not affected.
//
// BBS additionally excludes its H2C/H2D/X2D models by name; those are multi-nozzle machines
// already ruled out by the nozzle_diameter test above, so the name check is dropped here
// rather than carried over as a Bambu-specific special case.
bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const
{
warning_text.clear();
auto *nozzle_diameter_opt = config.option<ConfigOptionFloatsNullable>("nozzle_diameter");
if (!nozzle_diameter_opt || nozzle_diameter_opt->values.size() > 1)
return false;
auto *is_mixed_opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
if (!is_mixed_opt || !has_any_mixed_filament(is_mixed_opt->values))
return false;
auto is_mixed_slot = [&](int extruder_1based) {
size_t idx = (size_t)(extruder_1based - 1);
return idx < is_mixed_opt->values.size() && is_mixed_opt->values[idx];
};
const std::string mixed_warn_msg = _u8L("Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, "
"which may significantly increase waste and the risk of nozzle / waste-chute clogging.");
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) {
if (!contain_instance_totally(obj_idx, 0))
continue;
ModelObject *mo = m_model->objects[obj_idx];
int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1;
if (is_mixed_slot(obj_ext)) {
warning_text = mixed_warn_msg;
return true;
}
for (ModelVolume *mv : mo->volumes) {
int vol_ext = mv->config.has("extruder") ? mv->config.extruder() : obj_ext;
if (is_mixed_slot(vol_ext)) {
warning_text = mixed_warn_msg;
return true;
}
}
}
return false;
}
bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config)
{
bool has_pla = false;
+3
View File
@@ -354,6 +354,9 @@ public:
bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message);
bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector<int> &tpu_filaments);
bool check_mixture_of_pla_and_petg(const DynamicPrintConfig & config);
// Warns when a mixed-color filament is used on a single-nozzle printer, where every
// component switch costs a full filament change and purge.
bool check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const;
bool check_mixture_filament_compatible(const DynamicPrintConfig& config, std::string &error_msg);
bool check_compatible_of_nozzle_and_filament(const DynamicPrintConfig & config, const std::vector<std::string>& filament_presets, std::string& error_msg);
+1764 -8
View File
File diff suppressed because it is too large Load Diff
+24 -1
View File
@@ -86,6 +86,10 @@ using t_optgroups = std::vector <std::shared_ptr<ConfigOptionsGroup>>;
class Plater;
enum class ActionButtonType : int;
// Sentinel filament id meaning "use the slot the sidebar context menu was opened on"
// (Sidebar::priv::m_menu_filament_id) rather than an explicit index.
inline constexpr int kSidebarContextMenuFilamentId = -2;
#define EVT_PUBLISHING_START 1
#define EVT_PUBLISHING_STOP 2
@@ -188,7 +192,7 @@ public:
void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default
void change_filament(size_t from_id, size_t to_id); // 0 base
void edit_filament();
void add_custom_filament(wxColour new_col);
void add_custom_filament(wxColour new_col, const std::string& preset_name = std::string(), bool skip_preset_validation = false);
bool is_new_project_in_gcode3mf();
// BBS
void on_bed_type_change(BedType bed_type);
@@ -262,6 +266,20 @@ public:
std::vector<PlaterPresetComboBox*>& combos_filament();
void clear_combos_filament_badge();
void udpate_combos_filament_badge();
// Mixed-color filament sidebar section
void add_mixed_filament();
void edit_mixed_filament(size_t idx);
void delete_mixed_filament_at(size_t idx);
void decompose_filament_color(int filament_idx);
void recalc_filament_scroll_sizes();
void update_mixed_filament_list();
bool has_broken_mixed_filament() const;
bool has_broken_mixed_filament(const PartPlate* plate) const;
void collect_physical_filament_info(std::vector<std::string>& color_strs,
std::vector<std::string>& names,
std::vector<std::string>& types,
std::vector<size_t>* config_indices = nullptr);
Search::OptionsSearcher& get_searcher();
std::string& get_search_line();
void update_printer_thumbnail();
@@ -313,6 +331,11 @@ public:
const SLAPrint& sla_print() const;
SLAPrint& sla_print();
// Helper: returns config indices where filament_is_mixed == true
std::vector<size_t> mixed_filament_config_indices() const;
// Helper: returns config indices where filament_is_mixed == false
std::vector<size_t> physical_filament_config_indices() const;
int new_project(bool skip_confirm = false, bool silent = false, const wxString& project_name = wxString());
// BBS: save & backup
void load_project(wxString const & filename = "", wxString const & originfile = "-");
+16
View File
@@ -4,6 +4,7 @@
#include "PresetHints.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
@@ -3497,6 +3498,21 @@ void TabPrintModel::activate_selected_page(std::function<void()> throw_if_cancel
f->set_value(boost::any(), false);
}
}
if (m_type == Preset::TYPE_PLATE)
static_cast<TabPrintPlate *>(this)->update_mixed_filament_seq_state();
}
// A mixed-color slot resolves to a different physical filament per layer, so a
// user-defined filament print order cannot be honoured while one exists.
void TabPrintPlate::update_mixed_filament_seq_state()
{
if (!m_active_page) return;
auto &proj_cfg = m_preset_bundle->project_config;
auto *opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
bool has_mixed = opt && has_any_mixed_filament(opt->values);
toggle_option("first_layer_sequence_choice", !has_mixed);
toggle_option("other_layers_sequence_choice", !has_mixed);
}
void TabPrintModel::on_value_change(const std::string& opt_id, const boost::any& value)
+2
View File
@@ -545,6 +545,8 @@ public:
void build() override;
void reset_model_config() override;
int show_spiral_mode_settings_dialog(bool is_object_config) { return m_config_manipulation.show_spiral_mode_settings_dialog(is_object_config); }
// Disables the user-defined filament print order while a mixed-color filament exists.
void update_mixed_filament_seq_state();
protected:
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
File diff suppressed because it is too large Load Diff
+398
View File
@@ -0,0 +1,398 @@
#pragma once
#include "GUI_Utils.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "libslic3r/TexturePainting.hpp"
#include <wx/sizer.h>
#include <wx/stattext.h>
#include "Widgets/PopupWindow.hpp"
#include <wx/panel.h>
#include <wx/scrolwin.h>
#include <wx/textctrl.h>
#include "Widgets/SpinInput.hpp"
#include <wx/checkbox.h>
#include <wx/button.h>
#include "Widgets/Button.hpp"
#include <wx/glcanvas.h>
#include <wx/event.h>
#include <array>
#include <atomic>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include <string>
class GreenSlider;
namespace Slic3r { namespace GUI {
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent);
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent);
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent);
wxDECLARE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent);
enum class TextureImportState {
Idle,
Computing,
Ready,
Error
};
enum class TextureAutoMixMode {
CMYW,
RYBW
};
enum class TextureFilamentKind {
ExistingPhysical,
ExistingMixed,
NewPhysical,
NewMixed
};
struct TextureFilamentEntry {
TextureFilamentKind kind{TextureFilamentKind::ExistingPhysical};
int dialog_index{-1};
size_t project_config_index{size_t(-1)};
std::string color_hex;
std::string name;
std::string type;
std::string preset_name;
std::vector<unsigned int> mixed_components;
std::vector<int> mixed_ratios;
};
struct TextureNewMixedFilament {
int dialog_index{-1};
std::string color_hex;
std::vector<int> component_dialog_indices;
std::vector<int> ratios;
};
struct FilamentMappingRow {
int cluster_id = -1;
std::array<std::size_t, 3> source_color = {0, 0, 0};
std::string source_hex;
int target_filament_idx = 0;
wxPanel* source_panel = nullptr;
wxPanel* target_panel = nullptr;
};
class FilamentSelectPopup;
class AutoMixSelectPopup;
// Lightweight 3D preview panel using wxGLCanvas.
// Renders: original textured, multi-color, or filament-mapped.
class TexturePreviewCanvas : public wxGLCanvas
{
public:
enum class RenderMode { Original, MultiColor, FilamentMap };
TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs);
~TexturePreviewCanvas();
void set_mesh_data(
const std::vector<std::array<float, 3>>& vertices,
const std::vector<std::array<int, 3>>& indices);
void set_texture_data(
const std::vector<std::array<float, 2>>& uvs,
const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels);
void set_texture_render_data(
const std::vector<std::vector<unsigned char>>& tex_pixels_rgb,
const std::vector<int>& tex_widths,
const std::vector<int>& tex_heights,
const std::vector<std::array<std::array<float,2>, 3>>& face_uvs,
const std::vector<int>& face_tex_ids);
void set_painted_mesh_data(
const std::vector<std::array<float, 3>>& vertices,
const std::vector<std::array<int, 3>>& indices);
void set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
void set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
void set_filament_color_map(const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map);
void set_render_mode(RenderMode mode);
RenderMode get_render_mode() const { return m_mode; }
void set_computing_overlay(bool show);
void reset_view();
private:
void on_paint(wxPaintEvent& evt);
void on_size(wxSizeEvent& evt);
void on_mouse(wxMouseEvent& evt);
void ensure_gl_ready();
void render();
void render_mesh();
void render_textured_original();
void render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size);
void upload_reset_icon_textures();
unsigned int upload_reset_icon_texture(const std::string& icon_name);
wxRect reset_overlay_rect() const;
bool handle_reset_overlay_mouse(wxMouseEvent& evt);
void upload_textures();
void compute_smooth_normals();
void update_bounding_box();
wxGLContext* m_context = nullptr;
bool m_gl_initialized = false;
RenderMode m_mode = RenderMode::Original;
float m_zoom = 1.0f;
float m_rot_x = -30.0f;
float m_rot_y = 30.0f;
float m_pan_x = 0.0f;
float m_pan_y = 0.0f;
wxPoint m_last_mouse_pos;
enum class DragMode { None, Rotate, Pan };
DragMode m_drag_mode = DragMode::None;
std::vector<std::array<float, 3>> m_vertices;
std::vector<std::array<int, 3>> m_indices;
std::vector<std::array<float, 2>> m_uvs;
std::vector<std::array<float, 3>> m_painted_vertices;
std::vector<std::array<int, 3>> m_painted_indices;
std::vector<std::array<float, 3>> m_face_colors_rgb;
std::vector<std::array<float, 3>> m_original_face_colors_rgb;
std::vector<std::array<float, 3>> m_filament_colors_rgb;
std::map<std::array<std::size_t, 3>, std::array<float, 3>> m_color_map;
unsigned int m_tex_id = 0;
int m_tex_w = 0;
int m_tex_h = 0;
int m_tex_channels = 3;
bool m_tex_dirty = false;
std::vector<unsigned char> m_tex_data;
std::vector<unsigned int> m_gl_tex_ids;
std::vector<std::vector<unsigned char>> m_tex_pixels_rgb;
std::vector<int> m_tex_widths;
std::vector<int> m_tex_heights;
std::vector<std::array<std::array<float,2>, 3>> m_face_uvs;
std::vector<int> m_face_tex_ids;
bool m_multi_tex_dirty = false;
std::vector<std::array<float, 3>> m_vertex_normals;
std::array<float, 3> m_center = {0, 0, 0};
float m_radius = 1.0f;
unsigned int m_reset_icon_tex = 0;
unsigned int m_reset_icon_hover_tex = 0;
unsigned int m_reset_icon_dark_tex = 0;
unsigned int m_reset_icon_dark_hover_tex = 0;
bool m_reset_overlay_hovered = false;
bool m_reset_overlay_pressed = false;
};
class TextureImportDialog : public DPIDialog
{
public:
TextureImportDialog(wxWindow* parent,
const Slic3r::TexturedMesh& textured_mesh,
const std::vector<TextureFilamentEntry>& filament_entries,
std::function<bool()> initial_cancel_callback = {},
std::function<bool(int)> initial_progress_callback = {});
~TextureImportDialog();
int ShowModal() override;
void on_dpi_changed(const wxRect& suggested_rect) override;
Slic3r::PaintedMesh get_painted_mesh() const;
std::vector<Slic3r::FilamentMatch> get_matches() const;
bool was_skipped() const { return m_skipped; }
bool fallback_to_geometry_only() const { return m_fallback_to_geometry_only; }
// Colors of virtual filaments that need to be created after dialog confirmation.
// Index i corresponds to filament index (m_existing_filament_count + i).
const std::vector<std::array<float, 4>>& get_new_filament_colors() const { return m_new_filament_colors; }
const std::vector<std::string>& get_new_filament_preset_names() const { return m_new_filament_preset_names; }
const std::vector<TextureNewMixedFilament>& get_new_mixed_filaments() const { return m_new_mixed_filaments; }
const std::vector<TextureFilamentEntry>& get_filament_entries() const { return m_filament_entries; }
size_t get_existing_filament_count() const { return m_existing_filament_count; }
private:
void build_ui();
void build_preview_panel(wxWindow* parent, wxSizer* sizer);
void build_params_panel(wxWindow* parent, wxSizer* sizer);
void build_mapping_panel(wxWindow* parent, wxSizer* sizer);
void build_bottom_buttons(wxSizer* sizer);
void set_state(TextureImportState new_state);
void update_ui_for_state();
void start_computation(bool auto_color = false, bool initial = false);
void cancel_computation();
void on_computation_complete(wxCommandEvent& evt);
void on_computation_progress(wxCommandEvent& evt);
void on_computation_error(wxCommandEvent& evt);
void on_mesh_repair_decision_required(wxCommandEvent& evt);
void rebuild_mapping_rows();
void do_auto_match();
// Reorder m_current_matches into a canonical, predictable order (ascending
// filament_index, with unmapped entries pushed to the end). Used right
// after the initial computation so the first view the user sees has a
// stable, intuitive layout.
void sort_current_matches_by_filament_index();
// Reorder m_current_matches so they appear in the same order as
// `previous_matches` (keyed by cluster_index). Entries whose cluster_index
// was not present before are appended at the end, preserving their current
// relative order. Used when the user toggles auto-merge so the rows do not
// visually jump around. Assumes each cluster_index appears at most once in
// both vectors (this invariant is currently guaranteed by do_auto_match,
// which produces one match per cluster).
void restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches);
std::vector<Slic3r::FilamentMatch> build_matches_from_rows() const;
void update_filament_color_map();
void show_filament_popup(size_t row_index);
void dismiss_filament_popup();
void dismiss_filament_popup_on_wheel(wxMouseEvent& evt);
void show_auto_mix_popup();
void dismiss_auto_mix_popup();
void set_auto_mix_mode(TextureAutoMixMode mode);
void apply_auto_standard_mix(TextureAutoMixMode mode);
void reset_auto_mix();
void update_auto_mix_reset_visibility();
bool add_decomposed_mixed_filament(size_t row_index);
int add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex,
const std::string& preset_name = std::string());
int add_virtual_mixed_filament(const std::string& color_hex,
const std::vector<int>& component_dialog_indices,
const std::vector<int>& ratios);
size_t max_filament_count() const;
bool can_add_virtual_filament() const;
// Recomputes m_drop_warning_label visibility from m_filaments_dropped and
// m_state. Safe to call whether or not the label has been created yet.
// Visibility reflects ONLY the result of the most recent do_auto_match():
// if the latest match did not drop any cluster, the label is hidden even
// if a previous match had dropped (no historical accumulation).
void update_drop_warning_visibility();
void compact_used_virtual_filaments();
int find_closest_filament_index(const std::array<std::size_t, 3>& color) const;
void on_color_preset_clicked(wxCommandEvent& evt);
void on_color_slider_changed(wxCommandEvent& evt);
void on_color_spin_changed(wxCommandEvent& evt);
void on_color_spin_text_changed(wxCommandEvent& evt);
void on_smooth_slider_changed(wxCommandEvent& evt);
void on_smooth_spin_changed(wxCommandEvent& evt);
void on_smooth_spin_text_changed(wxCommandEvent& evt);
void on_apply_clicked(wxCommandEvent& evt);
void on_auto_merge_toggled(wxCommandEvent& evt);
void highlight_view_button(int view_index);
void on_skip_clicked(wxCommandEvent& evt);
void on_ok_clicked(wxCommandEvent& evt);
void set_color_count_value(int value, bool update_spin);
void set_smooth_value(int value, bool update_spin);
void preview_spin_text_value(SpinInput* spin, GreenSlider* slider, int& param,
int min_value, int max_value, const wxString& text,
std::function<void()> on_value_changed = {});
void update_color_count_preset_buttons();
bool has_valid_result() const;
bool is_params_dirty() const;
void update_confirm_button_state();
Slic3r::TexturedMesh m_textured_mesh;
std::vector<std::string> m_filament_color_strs; // existing + virtual
std::vector<std::string> m_filament_names; // existing + virtual
std::vector<std::array<float, 4>> m_filament_colors_rgba; // existing + virtual
std::vector<TextureFilamentEntry> m_filament_entries; // aligned with m_filament_colors_rgba
size_t m_existing_filament_count = 0;
std::vector<std::array<float, 4>> m_new_filament_colors; // only virtual (to be created)
std::vector<std::string> m_new_filament_preset_names; // only virtual, aligned with m_new_filament_colors
std::vector<TextureNewMixedFilament> m_new_mixed_filaments;
std::string m_default_virtual_filament_preset_name;
TextureImportState m_state = TextureImportState::Idle;
bool m_skipped = false;
bool m_fallback_to_geometry_only = false;
// True iff *the most recent* do_auto_match() ran into the global filament
// limit and had to drop one or more clusters. Reset to false on every
// do_auto_match() entry so it never accumulates across runs: a run that
// does not drop anything must observe false here, regardless of whether
// previous runs dropped. Drives the inline orange warning above the
// bottom buttons; never affects the mapping itself.
bool m_filaments_dropped = false;
bool m_auto_merge_enabled = true;
TextureAutoMixMode m_auto_mix_mode = TextureAutoMixMode::CMYW;
int m_auto_mix_font_point_size = 10;
Slic3r::PaintedMesh m_painted;
std::vector<Slic3r::FilamentMatch> m_current_matches;
std::unique_ptr<std::thread> m_worker;
std::atomic<bool> m_cancel_flag{false};
std::mutex m_result_mutex;
Slic3r::PaintedMesh m_pending_result;
std::function<bool()> m_initial_cancel_callback;
std::function<bool(int)> m_initial_progress_callback;
bool m_current_computation_initial = false;
bool m_initial_computation_pending = false;
bool m_initial_computation_cancelled = false;
bool m_initial_computation_failed = false;
bool m_initial_tooltips_set = false;
bool m_current_computation_auto_color = false;
Slic3r::TexturePaintingSettings::MeshRepairDecision m_mesh_repair_decision =
Slic3r::TexturePaintingSettings::MeshRepairDecision::Ask;
Button* m_btn_color_4 = nullptr;
Button* m_btn_color_8 = nullptr;
Button* m_btn_color_16 = nullptr;
Button* m_btn_color_auto = nullptr;
GreenSlider* m_color_slider = nullptr;
SpinInput* m_color_spin = nullptr;
GreenSlider* m_smooth_slider = nullptr;
SpinInput* m_smooth_spin = nullptr;
Button* m_btn_apply = nullptr;
wxCheckBox* m_auto_merge_cb = nullptr;
Button* m_btn_auto_mix = nullptr;
Button* m_btn_mix_reset = nullptr;
bool m_auto_mix_applied = false;
AutoMixSelectPopup* m_auto_mix_popup = nullptr;
wxScrolledWindow* m_mapping_scroll = nullptr;
wxBoxSizer* m_mapping_sizer = nullptr;
std::vector<FilamentMappingRow> m_mapping_rows;
FilamentSelectPopup* m_filament_popup = nullptr;
int m_filament_popup_row = -1;
int m_skip_next_filament_popup_row = -1;
TexturePreviewCanvas* m_preview_canvas = nullptr;
wxPanel* m_tab_panel = nullptr;
Button* m_btn_view_original = nullptr;
Button* m_btn_view_multicolor = nullptr;
ProgressDialog* m_progress_dlg = nullptr;
Button* m_btn_skip = nullptr;
Button* m_btn_ok = nullptr;
wxStaticText* m_drop_warning_label = nullptr;
int m_param_color_count = 4;
int m_param_smooth = 5;
int m_applied_color_count = -1;
int m_applied_smooth = -1;
wxStaticText* m_hint_label = nullptr;
static const int ID_COLOR_4 = wxID_HIGHEST + 200;
static const int ID_COLOR_8 = wxID_HIGHEST + 201;
static const int ID_COLOR_16 = wxID_HIGHEST + 202;
static const int ID_COLOR_AUTO = wxID_HIGHEST + 203;
static const int ID_BTN_APPLY = wxID_HIGHEST + 204;
static const int ID_BTN_SKIP = wxID_HIGHEST + 205;
static const int ID_VIEW_ORIGINAL = wxID_HIGHEST + 206;
static const int ID_VIEW_MULTICOLOR = wxID_HIGHEST + 207;
wxDECLARE_EVENT_TABLE();
};
}} // namespace Slic3r::GUI
+22 -6
View File
@@ -87,10 +87,18 @@ void ComboBox::SetSelection(int n)
return;
drop.SetSelection(n);
SetLabel(drop.GetValue());
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk())
SetIcon(items[drop.selection].icon_textctrl);
else
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) {
if (m_keep_drop_arrow) {
SetIcon("drop_down");
SetIcon_1(items[drop.selection].icon_textctrl);
} else {
SetIcon(items[drop.selection].icon_textctrl);
}
} else {
SetIcon("drop_down");
if (m_keep_drop_arrow)
SetIcon_1(wxNullBitmap);
}
if (drop.selection >= 0) {
SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap);
@@ -120,10 +128,18 @@ void ComboBox::SetValue(const wxString &value)
{
drop.SetValue(value);
SetLabel(value);
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk())
SetIcon(items[drop.selection].icon_textctrl);
else
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) {
if (m_keep_drop_arrow) {
SetIcon("drop_down");
SetIcon_1(items[drop.selection].icon_textctrl);
} else {
SetIcon(items[drop.selection].icon_textctrl);
}
} else {
SetIcon("drop_down");
if (m_keep_drop_arrow)
SetIcon_1(wxNullBitmap);
}
if (drop.selection >= 0) {
SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap);
+6
View File
@@ -16,6 +16,7 @@ class ComboBox : public wxWindowWithItems<TextInput, wxItemContainer>
bool drop_down = false;
bool text_off = false;
bool is_replace_text_to_image = false;
bool m_keep_drop_arrow = false; // When true, item icon goes to icon_1, keeping drop_down arrow
wxString replace_text;
wxString image_for_text;
@@ -31,6 +32,11 @@ public:
DropDown & GetDropDown() { return drop; }
// When true, item icon is shown as icon_1 (secondary), preserving drop_down arrow.
// Note: item bitmaps are set via raw wxBitmap (not ScalableBitmap), so they won't
// auto-rescale on DPI change. Caller should recreate items after DPI change.
void SetKeepDropArrow(bool keep) { m_keep_drop_arrow = keep; }
virtual bool SetFont(wxFont const & font) override;
public:
+4 -1
View File
@@ -360,6 +360,9 @@ void DropDown::render(wxDC &dc)
for (int i = 0; i < items.size(); ++i) {
auto &item = items[i];
int states2 = states;
// Dimmed items stay selectable but render greyed out (used by the mixed-filament
// dialog to show components that are already consumed by another mix).
bool is_dimmed = (item.style & DD_ITEM_STYLE_DIMMED) != 0;
if ((item.style & DD_ITEM_STYLE_DISABLED) != 0)
states2 &= ~StateColor::Enabled;
// Skip by group
@@ -427,7 +430,7 @@ void DropDown::render(wxDC &dc)
}
pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont());
dc.SetTextForeground(text_color.colorForStates(states2));
dc.SetTextForeground(is_dimmed ? wxColour(0xCE, 0xCE, 0xCE) : text_color.colorForStates(states2));
dc.DrawText(text, pt);
if (group.IsEmpty() && !item.group_key.IsEmpty()) {
auto szBmp = arrow_bitmap.GetBmpSize();
+1
View File
@@ -13,6 +13,7 @@
#define DD_ITEM_STYLE_SPLIT_ITEM 0x0001 // ----text----, text with horizontal line arounds
#define DD_ITEM_STYLE_DISABLED 0x0002 // ----text----, text with horizontal line arounds
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
+16
View File
@@ -9,6 +9,8 @@
#include "../GUI_Utils.hpp"
#endif
wxDEFINE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent);
BEGIN_EVENT_TABLE(SpinInput, StaticBox)
EVT_KEY_DOWN(SpinInput::keyPressed)
@@ -74,6 +76,7 @@ void SpinInput::Create(wxWindow *parent,
state_handler.attach_child(text_ctrl);
text_ctrl->Bind(wxEVT_KILL_FOCUS, &SpinInput::onTextLostFocus, this);
text_ctrl->Bind(wxEVT_TEXT_ENTER, &SpinInput::onTextEnter, this);
text_ctrl->Bind(wxEVT_TEXT, &SpinInput::onTextChanged, this);
text_ctrl->Bind(wxEVT_KEY_DOWN, &SpinInput::keyPressed, this);
text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu
button_inc = createButton(true);
@@ -300,6 +303,19 @@ void SpinInput::onTextEnter(wxCommandEvent &event)
ProcessEventLocally(event);
}
void SpinInput::onTextChanged(wxCommandEvent &event)
{
long value;
if (text_ctrl->GetValue().ToLong(&value)) {
wxCommandEvent e(EVT_SPINCTRL_TEXT, GetId());
e.SetEventObject(this);
e.SetInt((int) value);
e.SetString(text_ctrl->GetValue());
GetEventHandler()->ProcessEvent(e);
}
event.Skip();
}
void SpinInput::mouseWheelMoved(wxMouseEvent &event)
{
auto delta = event.GetWheelRotation() < 0 ? 1 : -1;
+5
View File
@@ -9,6 +9,10 @@
class Button;
// Fired on every keystroke that leaves a parseable integer in the field, so callers can
// react live rather than only on commit (wxEVT_SPINCTRL) or Enter. Ported from BambuStudio.
wxDECLARE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent);
class SpinInput : public wxNavigationEnabled<StaticBox>
{
wxSize labelSize;
@@ -98,6 +102,7 @@ private:
void keyPressed(wxKeyEvent& event);
void onTimer(wxTimerEvent &evnet);
void onTextLostFocus(wxEvent &event);
void onTextChanged(wxCommandEvent &event);
void onTextEnter(wxCommandEvent &event);
void sendSpinEvent();
+9
View File
@@ -139,6 +139,15 @@ void TextInput::SetIcon_1(const wxString &icon) {
Rescale();
}
// Set icon_1 from a raw bitmap. Note: won't auto-rescale on DPI change
// since ScalableBitmap::name() will be empty. Caller should re-set after DPI change.
void TextInput::SetIcon_1(const wxBitmap &icon) {
this->icon_1 = ScalableBitmap();
if (icon.IsOk())
this->icon_1.bmp() = icon;
Rescale();
}
void TextInput::SetLabelColor(StateColor const &color)
{
label_color = color;
+1
View File
@@ -54,6 +54,7 @@ public:
void SetIcon(const wxString & icon);
void SetIcon_1(const wxString &icon);
void SetIcon_1(const wxBitmap &icon);
void SetLabelColor(StateColor const &color);
+70 -4
View File
@@ -256,6 +256,41 @@ static std::vector<float> MatrixFlatten(const WipingDialog::VolumeMatrix& matrix
return vec;
}
// Mixed-color slots are virtual: they are never loaded into a tray and so have no flushing
// volumes of their own. The dialog therefore shows only the physical filaments, which means
// converting between the full config matrix (indexed by config slot) and a dense physical
// sub-matrix (indexed by row/column in the table).
static std::vector<double> extract_physical_sub_matrix(
const std::vector<double>& full_matrix, size_t full_n,
const std::vector<size_t>& indices)
{
size_t p = indices.size();
std::vector<double> sub(p * p, 0.0);
if (full_matrix.size() < full_n * full_n)
return sub;
for (size_t pi = 0; pi < p; ++pi)
for (size_t pj = 0; pj < p; ++pj)
sub[pi * p + pj] = full_matrix[indices[pi] * full_n + indices[pj]];
return sub;
}
// Write the edited physical sub-matrix back into a copy of the full matrix, leaving the
// entries that belong to mixed slots untouched.
static std::vector<double> expand_physical_to_full_matrix(
const std::vector<double>& sub_matrix,
const std::vector<size_t>& indices, size_t full_n,
const std::vector<double>& original_matrix)
{
std::vector<double> full = original_matrix;
if (full.size() < full_n * full_n)
return full;
size_t p = indices.size();
for (size_t pi = 0; pi < p; ++pi)
for (size_t pj = 0; pj < p; ++pj)
full[indices[pi] * full_n + indices[pj]] = sub_matrix[pi * p + pj];
return full;
}
wxString WipingDialog::BuildTableObjStr()
{
auto full_config = wxGetApp().preset_bundle->full_config();
@@ -265,9 +300,22 @@ wxString WipingDialog::BuildTableObjStr()
auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
auto nozzle_flush_dataset = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->values;
// Restrict the table to physical filaments; mixed slots have no flushing volumes.
m_physical_indices = wxGetApp().preset_bundle->physical_filament_config_indices();
const size_t full_n = filament_colors.size();
{
std::vector<std::string> physical_colors;
physical_colors.reserve(m_physical_indices.size());
for (size_t i : m_physical_indices)
if (i < filament_colors.size())
physical_colors.push_back(filament_colors[i]);
filament_colors = std::move(physical_colors);
}
std::vector<std::vector<double>> flush_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
flush_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num));
auto fm = get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num);
flush_matrixs.emplace_back(extract_physical_sub_matrix(fm, full_n, m_physical_indices));
}
flush_multiplier.resize(nozzle_num, 1);
@@ -372,7 +420,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
this->SetSizer(main_sizer);
this->SetBackgroundColour(*wxWHITE);
auto filament_count = wxGetApp().preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
auto filament_count = wxGetApp().preset_bundle->physical_filament_config_indices().size();
// Estimate table scroll area size based on filament count
// Each table cell is ~60x25 DIP, plus headers and borders
@@ -592,11 +640,29 @@ void WipingDialog::StoreFlushData(int extruder_num, const std::vector<std::vecto
m_raw_matrixs = flush_volume_vecs;
}
// The table edits a physical-only sub-matrix; GetFlattenMatrix has to hand back a full-size
// matrix so the config layout stays indexed by config slot. Mixed-slot entries keep whatever
// the config already held.
std::vector<double> WipingDialog::ExpandToFullMatrix(const std::vector<double>& sub_matrix, int nozzle_idx) const
{
const auto& project_config = wxGetApp().preset_bundle->project_config;
const size_t full_n = project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
if (m_physical_indices.size() == full_n)
return sub_matrix; // no mixed slots: sub-matrix already is the full matrix
auto raw = project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
int nozzle_num = (int)wxGetApp().preset_bundle->project_config.option<ConfigOptionFloats>("flush_multiplier")->values.size();
if (nozzle_num < 1) nozzle_num = 1;
auto original = get_flush_volumes_matrix(raw, nozzle_idx, nozzle_num);
return expand_physical_to_full_matrix(sub_matrix, m_physical_indices, full_n, original);
}
std::vector<double> WipingDialog::GetFlattenMatrix()const
{
std::vector<double> ret;
for (auto& matrix : m_raw_matrixs) {
ret.insert(ret.end(), matrix.begin(), matrix.end());
for (size_t idx = 0; idx < m_raw_matrixs.size(); ++idx) {
auto full = ExpandToFullMatrix(m_raw_matrixs[idx], (int)idx);
ret.insert(ret.end(), full.begin(), full.end());
}
return ret;
}
+4
View File
@@ -58,12 +58,16 @@ private:
wxString BuildTableObjStr();
wxString BuildTextObjStr(bool multi_language = true);
void StoreFlushData(int extruder_num, const std::vector<std::vector<double>>& flush_volume_vecs, const std::vector<double>& flush_multipliers);
// Maps the physical-only matrix shown in the table back onto the full config-indexed matrix.
std::vector<double> ExpandToFullMatrix(const std::vector<double>& sub_matrix, int nozzle_idx) const;
wxWebView* m_webview;
int m_max_flush_volume;
VolumeMatrix m_raw_matrixs;
std::vector<double> m_flush_multipliers;
// Config indices of the physical (non-mixed) filaments, in table order.
std::vector<size_t> m_physical_indices;
bool m_submit_flag{ false };
};
+1
View File
@@ -20,6 +20,7 @@ add_executable(${_TEST_NAME}_tests
test_vendor_cache.cpp
test_elephant_foot_compensation.cpp
test_fill_corner_smoothing.cpp
test_filament_mixer.cpp
test_fill_plane_path.cpp
test_geometry.cpp
test_multimaterial_segmentation.cpp
+182
View File
@@ -0,0 +1,182 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/FilamentMixer.hpp"
using namespace Slic3r;
TEST_CASE("parse_mixed_components reads 1-based component ids", "[FilamentMixer]")
{
REQUIRE(parse_mixed_components("1,3") == std::vector<unsigned int>{1, 3});
REQUIRE(parse_mixed_components("2, 4 ,5") == std::vector<unsigned int>{2, 4, 5});
SECTION("Malformed input yields no components") {
REQUIRE(parse_mixed_components("").empty());
REQUIRE(parse_mixed_components("abc").empty());
}
}
TEST_CASE("parse_mixed_ratios normalizes to sum 1.0", "[FilamentMixer]")
{
auto r = parse_mixed_ratios("0.7,0.3", 2);
REQUIRE(r.size() == 2);
REQUIRE_THAT(r[0], Catch::Matchers::WithinAbs(0.7, 1e-9));
REQUIRE_THAT(r[1], Catch::Matchers::WithinAbs(0.3, 1e-9));
SECTION("Unnormalized input is rescaled") {
auto v = parse_mixed_ratios("2,2", 2);
REQUIRE_THAT(v[0], Catch::Matchers::WithinAbs(0.5, 1e-9));
REQUIRE_THAT(v[1], Catch::Matchers::WithinAbs(0.5, 1e-9));
}
SECTION("Empty or mismatched input falls back to equal shares") {
auto v = parse_mixed_ratios("", 3);
REQUIRE(v.size() == 3);
for (double x : v)
REQUIRE_THAT(x, Catch::Matchers::WithinAbs(1.0 / 3.0, 1e-9));
}
}
TEST_CASE("has_any_mixed_filament detects mixed slots", "[FilamentMixer]")
{
REQUIRE_FALSE(has_any_mixed_filament({}));
REQUIRE_FALSE(has_any_mixed_filament({0, 0, 0}));
REQUIRE(has_any_mixed_filament({0, 1, 0}));
}
TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", "[FilamentMixer]")
{
// Slot 2 (0-based) is a mix of physical filaments 1 and 2 (1-based) => 0 and 1 (0-based).
const std::vector<unsigned char> is_mixed = {0, 0, 1};
const std::vector<std::string> comp_strs = {"", "", "1,2"};
REQUIRE(expand_mixed_filaments({2}, is_mixed, comp_strs) == std::vector<unsigned int>{0, 1});
SECTION("Non-mixed entries pass through, result is sorted and deduplicated") {
REQUIRE(expand_mixed_filaments({2, 0}, is_mixed, comp_strs) == std::vector<unsigned int>{0, 1});
}
}
TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]")
{
const std::vector<unsigned char> is_mixed = {0, 0, 1};
SECTION("All components resolve") {
REQUIRE(check_mixed_filament_integrity(is_mixed, {"", "", "1,2"}, 2).empty());
}
SECTION("A component past the physical filament count is broken") {
auto broken = check_mixed_filament_integrity(is_mixed, {"", "", "1,9"}, 2);
REQUIRE(broken == std::vector<size_t>{2});
}
}
TEST_CASE("remap_mixed_components_on_delete rewrites ids around the deleted slot", "[FilamentMixer]")
{
const std::vector<unsigned char> is_mixed = {0, 0, 0, 1};
std::vector<std::string> comps = {"", "", "", "1,3"};
SECTION("Deleting a filament below the references shifts them down") {
remap_mixed_components_on_delete(is_mixed, comps, 2);
REQUIRE(comps[3] == "1,2");
}
SECTION("Deleting a referenced filament zeroes that component") {
remap_mixed_components_on_delete(is_mixed, comps, 1);
// 1 -> 0 (deleted sentinel), 3 -> 2
REQUIRE(comps[3] == "0,2");
}
}
TEST_CASE("check_mixed_filament_type_consistency flags mismatched component types", "[FilamentMixer]")
{
const std::vector<unsigned char> is_mixed = {0, 0, 1};
const std::vector<std::string> comp_strs = {"", "", "1,2"};
REQUIRE(check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PLA"}).empty());
auto bad = check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PETG"});
REQUIRE(bad == std::vector<size_t>{2});
}
TEST_CASE("gradient curves round-trip and sample monotonically", "[FilamentMixer]")
{
SECTION("Empty input yields an empty curve") {
REQUIRE(parse_gradient_curve("").empty());
REQUIRE(serialize_gradient_curve(GradientCurve{}).empty());
}
SECTION("Legacy 2-field anchors survive a parse/serialize round trip") {
GradientCurve c = parse_gradient_curve("0,0.15|0.5,0.5|1,0.85");
REQUIRE(c.points.size() == 3);
// Anchors with no tangent override serialize back to the 2-field legacy form
// (canonical fixed-precision, so compare by re-parsing rather than by string).
const std::string round_tripped = serialize_gradient_curve(c);
REQUIRE(round_tripped.find(",nan") == std::string::npos);
GradientCurve c2 = parse_gradient_curve(round_tripped);
REQUIRE(c2.points.size() == c.points.size());
for (size_t i = 0; i < c.points.size(); ++i) {
REQUIRE_THAT(c2.points[i].x, Catch::Matchers::WithinAbs(c.points[i].x, 1e-4));
REQUIRE_THAT(c2.points[i].y, Catch::Matchers::WithinAbs(c.points[i].y, 1e-4));
}
}
SECTION("Sampling is clamped at the ends and monotone in between") {
GradientCurve c = parse_gradient_curve("0,0.15|0.5,0.5|1,0.85");
REQUIRE_THAT(sample_gradient_curve(c, 0.0), Catch::Matchers::WithinAbs(0.15, 1e-9));
REQUIRE_THAT(sample_gradient_curve(c, 1.0), Catch::Matchers::WithinAbs(0.85, 1e-9));
// Outside the control point range the end values are held.
REQUIRE_THAT(sample_gradient_curve(c, -1.0), Catch::Matchers::WithinAbs(0.15, 1e-9));
REQUIRE_THAT(sample_gradient_curve(c, 2.0), Catch::Matchers::WithinAbs(0.85, 1e-9));
double prev = sample_gradient_curve(c, 0.0);
for (int i = 1; i <= 20; ++i) {
double v = sample_gradient_curve(c, i / 20.0);
REQUIRE(v >= prev - 1e-9);
prev = v;
}
}
SECTION("A curve with fewer than two points falls back to 0.5") {
GradientCurve c = parse_gradient_curve("0.5,0.7");
REQUIRE_THAT(sample_gradient_curve(c, 0.3), Catch::Matchers::WithinAbs(0.5, 1e-9));
}
}
TEST_CASE("blend_color mixes two hex colors", "[FilamentMixer]")
{
// ratio 0 keeps the first color, ratio 1 the second.
REQUIRE(blend_color("#FF0000", "#0000FF", 0.0f) == "#FF0000");
REQUIRE(blend_color("#FF0000", "#0000FF", 1.0f) == "#0000FF");
SECTION("Blue and yellow make green, not grey (pigment mixing)") {
// The polynomial model approximates subtractive pigment behaviour.
std::string mixed = blend_color("#0021D0", "#FCD300", 0.5f);
REQUIRE(mixed.size() == 7);
REQUIRE(mixed[0] == '#');
auto comp = [&](int i) { return std::stoi(mixed.substr(1 + 2 * i, 2), nullptr, 16); };
// Green channel should dominate red and blue.
REQUIRE(comp(1) > comp(0));
REQUIRE(comp(1) > comp(2));
}
}
TEST_CASE("blend_color_multi weights components", "[FilamentMixer]")
{
SECTION("A single component is returned unchanged") {
REQUIRE(blend_color_multi({"#FF0000"}, {1}) == "#FF0000");
}
SECTION("Mixing a color with itself stays close to that color") {
// The mixer is a degree-4 polynomial fit of pigment behaviour, so a round trip through
// it is near-identity rather than exact (the model documents a mean Delta-E around 2).
std::string mixed = blend_color_multi({"#123456", "#123456"}, {1, 1});
REQUIRE(mixed.size() == 7);
auto comp = [](const std::string &hex, int i) {
return std::stoi(hex.substr(1 + 2 * i, 2), nullptr, 16);
};
for (int i = 0; i < 3; ++i)
REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8);
}
}
@@ -566,3 +566,46 @@ TEST_CASE("A printer specific filament supersedes the generic library filament w
CHECK(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_c, nullptr)));
}
// Mixed-color filament metadata lives in project_config as parallel per-filament arrays.
// set_num_filaments() is the single place that grows them alongside filament_colour; if it
// misses them, creating a mixed slot writes past the end of the short arrays.
TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament count", "[Preset][Bundle][FilamentMixer]")
{
static const char *kMixedKeys[] = {
"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",
};
auto mixed_array_size = [](const DynamicPrintConfig &cfg, const std::string &key) -> size_t {
if (const auto *b = cfg.option<ConfigOptionBools>(key))
return b->values.size();
if (const auto *s = cfg.option<ConfigOptionStrings>(key))
return s->values.size();
return size_t(-1); // key missing entirely
};
PresetBundle bundle;
const unsigned int n = GENERATE(2u, 4u, 8u);
bundle.set_num_filaments(n, std::string("#FF0000"));
REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == n);
for (const char *key : kMixedKeys) {
DYNAMIC_SECTION("grown: " << key) {
CHECK(mixed_array_size(bundle.project_config, key) == n);
}
}
SECTION("shrinking keeps them in step too") {
bundle.set_num_filaments(1, std::string("#00FF00"));
REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == 1);
for (const char *key : kMixedKeys)
CHECK(mixed_array_size(bundle.project_config, key) == 1);
}
}