mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-09 18:27:00 +00:00
Port color decompose recipe data and interpolation from BambuStudio
This commit is contained in:
@@ -61,6 +61,42 @@ static LabColor rgb_to_lab(const ColorDecomposeRgb& rgb)
|
||||
return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)};
|
||||
}
|
||||
|
||||
static std::string lab_to_srgb_hex(const LabColor& lab)
|
||||
{
|
||||
constexpr double Xn = 0.95047, Yn = 1.0, Zn = 1.08883;
|
||||
|
||||
auto f_inv = [](double t) -> double {
|
||||
constexpr double eps = 216.0 / 24389.0;
|
||||
constexpr double kappa = 24389.0 / 27.0;
|
||||
const double t3 = t * t * t;
|
||||
return t3 > eps ? t3 : (t * 116.0 - 16.0) / kappa;
|
||||
};
|
||||
|
||||
const double fy = (lab.l + 16.0) / 116.0;
|
||||
const double fx = lab.a / 500.0 + fy;
|
||||
const double fz = fy - lab.b / 200.0;
|
||||
|
||||
const double X = Xn * f_inv(fx);
|
||||
const double Y = Yn * f_inv(fy);
|
||||
const double Z = Zn * f_inv(fz);
|
||||
|
||||
double r = 3.2406 * X - 1.5372 * Y - 0.4986 * Z;
|
||||
double g = -0.9689 * X + 1.8758 * Y + 0.0415 * Z;
|
||||
double b = 0.0557 * X - 0.2040 * Y + 1.0570 * Z;
|
||||
|
||||
auto gamma = [](double c) -> double {
|
||||
c = std::max(0.0, std::min(1.0, c));
|
||||
return c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055;
|
||||
};
|
||||
auto u8 = [&](double c) -> int {
|
||||
return std::max(0, std::min(255, static_cast<int>(std::lround(gamma(c) * 255.0))));
|
||||
};
|
||||
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", u8(r), u8(g), u8(b));
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
static double delta_e76(const LabColor& a, const LabColor& b)
|
||||
{
|
||||
return std::sqrt(std::pow(a.l - b.l, 2.0) + std::pow(a.a - b.a, 2.0) + std::pow(a.b - b.b, 2.0));
|
||||
@@ -224,6 +260,29 @@ ColorDecomposeRecipeResult recommend_from_physical_filaments(
|
||||
if (preferred_material_type.empty() || material_matches(filament.type, preferred_material_type))
|
||||
candidates.push_back(filament);
|
||||
}
|
||||
|
||||
// Early exit: if a material-matched candidate has the exact target color,
|
||||
// return it as 100%. Downstream rejects single-component results (no mixed
|
||||
// slot created), which is correct -- the color already exists.
|
||||
const std::string target_hex = color_decompose_rgb_to_hex(target);
|
||||
for (const auto& cand : candidates) {
|
||||
ColorDecomposeRgb cand_rgb;
|
||||
if (!color_decompose_hex_to_rgb(cand.color_hex, cand_rgb))
|
||||
continue;
|
||||
if (color_decompose_rgb_to_hex(cand_rgb) == target_hex) {
|
||||
ColorDecomposeRecipeResult exact;
|
||||
exact.valid = true;
|
||||
exact.mode = ColorDecomposeRecipeMode::MaterialList;
|
||||
exact.matched_color_hex = cand.color_hex;
|
||||
ColorDecomposeRecipeComponent comp;
|
||||
comp.color_hex = cand.color_hex;
|
||||
comp.ratio = 100;
|
||||
comp.filament_index = cand.filament_index;
|
||||
exact.components.push_back(comp);
|
||||
return exact;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size() < 2)
|
||||
candidates = physical_filaments;
|
||||
candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [](const auto& filament) {
|
||||
@@ -328,34 +387,144 @@ std::string lookup_measured_blend_color(const std::vector<std::string>& componen
|
||||
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())
|
||||
// Stage 1: canonicalize input by sorting (hex, ratio) pairs so matching
|
||||
// is independent of the caller's component order.
|
||||
const size_t n = component_hexes.size();
|
||||
std::vector<std::pair<std::string, int>> in_pairs;
|
||||
in_pairs.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
std::string nh = normalize_hex(component_hexes[i]);
|
||||
if (nh.empty())
|
||||
return {};
|
||||
norm_hexes.push_back(std::move(n));
|
||||
in_pairs.emplace_back(std::move(nh), ratios[i]);
|
||||
}
|
||||
std::sort(in_pairs.begin(), in_pairs.end());
|
||||
|
||||
std::vector<std::string> in_hexes;
|
||||
std::vector<int> in_ratios;
|
||||
in_hexes.reserve(n);
|
||||
in_ratios.reserve(n);
|
||||
for (const auto& p : in_pairs) {
|
||||
in_hexes.push_back(p.first);
|
||||
in_ratios.push_back(p.second);
|
||||
}
|
||||
|
||||
// Normalize ratios to sum=100 (callers may pass arbitrary weights,
|
||||
// e.g. MixedFilamentDialog uses ratio*10000).
|
||||
{
|
||||
int sum = 0;
|
||||
for (int r : in_ratios) sum += r;
|
||||
if (sum > 0 && sum != 100) {
|
||||
int new_sum = 0;
|
||||
for (size_t i = 0; i < in_ratios.size(); ++i) {
|
||||
in_ratios[i] = static_cast<int>(std::lround(
|
||||
static_cast<double>(in_ratios[i]) * 100.0 / static_cast<double>(sum)));
|
||||
new_sum += in_ratios[i];
|
||||
}
|
||||
if (new_sum != 100) {
|
||||
auto it = std::max_element(in_ratios.begin(), in_ratios.end());
|
||||
*it += (100 - new_sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to polynomial model for ratios outside the measured range.
|
||||
{
|
||||
bool out_of_range = false;
|
||||
if (n == 2) {
|
||||
for (int r : in_ratios)
|
||||
if (r < 20 || r > 80) { out_of_range = true; break; }
|
||||
} else {
|
||||
for (int r : in_ratios)
|
||||
if (r < 20) { out_of_range = true; break; }
|
||||
}
|
||||
if (out_of_range)
|
||||
return {};
|
||||
}
|
||||
|
||||
// Stage 2: collect anchors with the same component hex set; try exact match.
|
||||
struct Anchor {
|
||||
std::vector<int> ratios;
|
||||
LabColor lab;
|
||||
std::string hex;
|
||||
};
|
||||
std::vector<Anchor> anchors;
|
||||
|
||||
for (const StandardRecipeEntry& entry : standard_entries()) {
|
||||
if (entry.source != "measured" && entry.source != "interpolated")
|
||||
continue;
|
||||
if (entry.component_hexes.size() != norm_hexes.size())
|
||||
continue;
|
||||
if (entry.ratios != ratios)
|
||||
if (entry.component_hexes.size() != n)
|
||||
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;
|
||||
std::vector<std::pair<std::string, int>> e_pairs;
|
||||
e_pairs.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
e_pairs.emplace_back(normalize_hex(entry.component_hexes[i]), entry.ratios[i]);
|
||||
std::sort(e_pairs.begin(), e_pairs.end());
|
||||
|
||||
bool same_set = true;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (e_pairs[i].first != in_hexes[i]) { same_set = false; break; }
|
||||
if (!same_set)
|
||||
continue;
|
||||
|
||||
Anchor a;
|
||||
a.ratios.reserve(n);
|
||||
for (const auto& p : e_pairs) a.ratios.push_back(p.second);
|
||||
a.lab = entry.measured_lab;
|
||||
a.hex = entry.measured_hex;
|
||||
|
||||
if (a.ratios == in_ratios)
|
||||
return a.hex;
|
||||
|
||||
anchors.push_back(std::move(a));
|
||||
}
|
||||
return {};
|
||||
|
||||
if (anchors.size() < 2)
|
||||
return {};
|
||||
|
||||
// Stage 3: interpolation in Lab space.
|
||||
if (n == 2) {
|
||||
// 1D linear interpolation along ratio[0].
|
||||
std::sort(anchors.begin(), anchors.end(),
|
||||
[](const Anchor& a, const Anchor& b) { return a.ratios[0] < b.ratios[0]; });
|
||||
const double x = static_cast<double>(in_ratios[0]);
|
||||
size_t lo = 0;
|
||||
while (lo + 2 < anchors.size() && static_cast<double>(anchors[lo + 1].ratios[0]) <= x)
|
||||
++lo;
|
||||
const Anchor& a0 = anchors[lo];
|
||||
const Anchor& a1 = anchors[lo + 1];
|
||||
const double span = static_cast<double>(a1.ratios[0] - a0.ratios[0]);
|
||||
const double t = span > 0.0 ? (x - static_cast<double>(a0.ratios[0])) / span : 0.0;
|
||||
return lab_to_srgb_hex({a0.lab.l + t * (a1.lab.l - a0.lab.l),
|
||||
a0.lab.a + t * (a1.lab.a - a0.lab.a),
|
||||
a0.lab.b + t * (a1.lab.b - a0.lab.b)});
|
||||
}
|
||||
|
||||
// 3+ color: IDW (p=2) with 3 nearest anchors in the (ratio[0], ratio[1]) plane.
|
||||
const double ra = static_cast<double>(in_ratios[0]);
|
||||
const double rb = static_cast<double>(in_ratios[1]);
|
||||
std::vector<std::pair<double, const Anchor*>> dists;
|
||||
dists.reserve(anchors.size());
|
||||
for (const Anchor& a : anchors) {
|
||||
const double d = std::sqrt(std::pow(ra - static_cast<double>(a.ratios[0]), 2.0) +
|
||||
std::pow(rb - static_cast<double>(a.ratios[1]), 2.0));
|
||||
if (d == 0.0)
|
||||
return a.hex;
|
||||
dists.emplace_back(d, &a);
|
||||
}
|
||||
const size_t k = std::min(static_cast<size_t>(3), dists.size());
|
||||
std::partial_sort(dists.begin(), dists.begin() + k, dists.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
double num_l = 0.0, num_a = 0.0, num_b = 0.0, den = 0.0;
|
||||
for (size_t j = 0; j < k; ++j) {
|
||||
const double w = 1.0 / (dists[j].first * dists[j].first);
|
||||
num_l += w * dists[j].second->lab.l;
|
||||
num_a += w * dists[j].second->lab.a;
|
||||
num_b += w * dists[j].second->lab.b;
|
||||
den += w;
|
||||
}
|
||||
return lab_to_srgb_hex({num_l / den, num_a / den, num_b / den});
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -652,7 +652,6 @@ void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
m_limit_warning_panel->Hide();
|
||||
Layout();
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -678,12 +677,11 @@ void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
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.
|
||||
// Only resize when the warning panel actually toggled from hidden to shown.
|
||||
// While already visible, switching modes must not re-Fit the dialog, which
|
||||
// would make it jump on every card switch. Fit keeps the user-moved position.
|
||||
if (!was_shown) {
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4544,7 +4544,13 @@ void Sidebar::collect_physical_filament_info(std::vector<std::string>& color_str
|
||||
Preset* preset = nullptr;
|
||||
if (cfg_idx < preset_bundle.filament_presets.size())
|
||||
preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]);
|
||||
types.push_back(filament_type_for_color_decompose(preset));
|
||||
std::string ft;
|
||||
if (preset) {
|
||||
std::string display_type;
|
||||
ft = preset->config.get_filament_type(display_type);
|
||||
}
|
||||
if (ft.empty()) ft = "PLA";
|
||||
types.push_back(ft);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4905,6 +4911,21 @@ void Sidebar::decompose_filament_color(int filament_idx)
|
||||
std::vector<std::string> color_strs, names, types;
|
||||
std::vector<size_t> physical_config_indices;
|
||||
collect_physical_filament_info(color_strs, names, types, &physical_config_indices);
|
||||
|
||||
// Build decompose-specific types: ColorDecomposeDialog needs "PLA Basic"
|
||||
// distinction (for CMYW/RYBW card visibility), while collect_physical_filament_info
|
||||
// now returns coarse filament_type (e.g. "PLA" for all PLA variants).
|
||||
std::vector<std::string> decompose_types;
|
||||
{
|
||||
auto& pb = *wxGetApp().preset_bundle;
|
||||
for (size_t i = 0; i < physical_config_indices.size(); ++i) {
|
||||
const size_t ci = physical_config_indices[i];
|
||||
Preset* pr = (ci < pb.filament_presets.size())
|
||||
? pb.filaments.find_preset(pb.filament_presets[ci]) : nullptr;
|
||||
decompose_types.push_back(filament_type_for_color_decompose(pr));
|
||||
}
|
||||
}
|
||||
|
||||
size_t source_physical_idx = size_t(-1);
|
||||
for (size_t i = 0; i < physical_config_indices.size(); ++i) {
|
||||
if (physical_config_indices[i] == static_cast<size_t>(filament_idx)) {
|
||||
@@ -4915,7 +4936,7 @@ void Sidebar::decompose_filament_color(int filament_idx)
|
||||
|
||||
ColorDecomposeDialog dlg(this,
|
||||
source_physical_idx == size_t(-1) ? -1 : static_cast<int>(source_physical_idx),
|
||||
target_color, color_strs, names, types,
|
||||
target_color, color_strs, names, decompose_types,
|
||||
wxGetApp().preset_bundle->filament_presets.size(),
|
||||
static_cast<size_t>(EnforcerBlockerType::ExtruderMax),
|
||||
physical_config_indices);
|
||||
@@ -4925,7 +4946,7 @@ void Sidebar::decompose_filament_color(int filament_idx)
|
||||
MixedFilamentResult mixed_result;
|
||||
std::vector<DecomposeMissingComponent> missing_components;
|
||||
if (!prepare_decompose_mixed_result(dialog_result, static_cast<size_t>(filament_idx), source_physical_idx,
|
||||
color_strs, types, physical_config_indices, mixed_result, missing_components))
|
||||
color_strs, decompose_types, physical_config_indices, mixed_result, missing_components))
|
||||
return;
|
||||
|
||||
if (!confirm_create_decompose_missing_components(this, missing_components))
|
||||
|
||||
Reference in New Issue
Block a user