mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-17 05:52:39 +00:00
Merge main + clean up code + fix missing include
This commit is contained in:
@@ -179,6 +179,17 @@ set(lisbslic3r_sources
|
||||
Fill/Lightning/Layer.hpp
|
||||
Fill/Lightning/TreeNode.cpp
|
||||
Fill/Lightning/TreeNode.hpp
|
||||
FilamentMixer.cpp
|
||||
FilamentMixer.hpp
|
||||
FilamentMixerModel.hpp
|
||||
ColorDecomposeRecipe.cpp
|
||||
ColorDecomposeRecipe.hpp
|
||||
TexturePainting.hpp
|
||||
TexturePainting.cpp
|
||||
TextureToColor/TextureToColor.hpp
|
||||
TextureToColor/TextureToColor.cpp
|
||||
TextureToColor/ColorUtils.hpp
|
||||
TextureToColor/ColorUtils.cpp
|
||||
Flow.cpp
|
||||
Flow.hpp
|
||||
FlushVolCalc.cpp
|
||||
@@ -194,6 +205,9 @@ set(lisbslic3r_sources
|
||||
format.hpp
|
||||
Format/OBJ.cpp
|
||||
Format/OBJ.hpp
|
||||
Format/AssimpImport.hpp
|
||||
Format/AssimpImport.cpp
|
||||
Format/ResourcePathUtils.hpp
|
||||
Format/objparser.cpp
|
||||
Format/objparser.hpp
|
||||
Format/SL1.cpp
|
||||
@@ -511,6 +525,7 @@ cmake_policy(SET CMP0011 NEW)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0167 NEW)
|
||||
find_package(CGAL REQUIRED)
|
||||
find_package(OpenCV REQUIRED core)
|
||||
find_package(assimp REQUIRED)
|
||||
unset(CMAKE_POLICY_DEFAULT_CMP0167)
|
||||
cmake_policy(POP)
|
||||
|
||||
@@ -551,7 +566,7 @@ target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTI
|
||||
if (USE_SLIC3R_CONSOLE_LOG)
|
||||
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
|
||||
endif()
|
||||
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/TextureToColor PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS})
|
||||
|
||||
# Find the OCCT and related libraries
|
||||
@@ -599,6 +614,7 @@ target_link_libraries(libslic3r
|
||||
libnest2d
|
||||
miniz
|
||||
opencv_world
|
||||
assimp::assimp
|
||||
PRIVATE
|
||||
${CMAKE_DL_LIBS}
|
||||
${EXPAT_LIBRARIES}
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
#include "ColorDecomposeRecipe.hpp"
|
||||
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
struct LabColor {
|
||||
double l{0.0};
|
||||
double a{0.0};
|
||||
double b{0.0};
|
||||
};
|
||||
|
||||
struct StandardRecipeEntry {
|
||||
ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::CMYW};
|
||||
std::string material;
|
||||
std::string source;
|
||||
std::vector<std::string> component_keys;
|
||||
std::vector<std::string> component_hexes;
|
||||
std::vector<int> ratios;
|
||||
std::string measured_hex;
|
||||
LabColor measured_lab;
|
||||
};
|
||||
|
||||
static double srgb_to_linear(double v)
|
||||
{
|
||||
v /= 255.0;
|
||||
return v <= 0.04045 ? v / 12.92 : std::pow((v + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
static double xyz_to_lab_component(double v)
|
||||
{
|
||||
constexpr double eps = 216.0 / 24389.0;
|
||||
constexpr double kappa = 24389.0 / 27.0;
|
||||
return v > eps ? std::cbrt(v) : (kappa * v + 16.0) / 116.0;
|
||||
}
|
||||
|
||||
static LabColor rgb_to_lab(const ColorDecomposeRgb& rgb)
|
||||
{
|
||||
const double r = srgb_to_linear(rgb.r);
|
||||
const double g = srgb_to_linear(rgb.g);
|
||||
const double b = srgb_to_linear(rgb.b);
|
||||
|
||||
const double x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
|
||||
const double y = (0.2126729 * r + 0.7151522 * g + 0.0721750 * b);
|
||||
const double z = (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / 1.08883;
|
||||
|
||||
const double fx = xyz_to_lab_component(x);
|
||||
const double fy = xyz_to_lab_component(y);
|
||||
const double fz = xyz_to_lab_component(z);
|
||||
|
||||
return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)};
|
||||
}
|
||||
|
||||
static std::string lab_to_srgb_hex(const LabColor& lab)
|
||||
{
|
||||
constexpr double Xn = 0.95047, Yn = 1.0, Zn = 1.08883;
|
||||
|
||||
auto f_inv = [](double t) -> double {
|
||||
constexpr double eps = 216.0 / 24389.0;
|
||||
constexpr double kappa = 24389.0 / 27.0;
|
||||
const double t3 = t * t * t;
|
||||
return t3 > eps ? t3 : (t * 116.0 - 16.0) / kappa;
|
||||
};
|
||||
|
||||
const double fy = (lab.l + 16.0) / 116.0;
|
||||
const double fx = lab.a / 500.0 + fy;
|
||||
const double fz = fy - lab.b / 200.0;
|
||||
|
||||
const double X = Xn * f_inv(fx);
|
||||
const double Y = Yn * f_inv(fy);
|
||||
const double Z = Zn * f_inv(fz);
|
||||
|
||||
double r = 3.2406 * X - 1.5372 * Y - 0.4986 * Z;
|
||||
double g = -0.9689 * X + 1.8758 * Y + 0.0415 * Z;
|
||||
double b = 0.0557 * X - 0.2040 * Y + 1.0570 * Z;
|
||||
|
||||
auto gamma = [](double c) -> double {
|
||||
c = std::max(0.0, std::min(1.0, c));
|
||||
return c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055;
|
||||
};
|
||||
auto u8 = [&](double c) -> int {
|
||||
return std::max(0, std::min(255, static_cast<int>(std::lround(gamma(c) * 255.0))));
|
||||
};
|
||||
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", u8(r), u8(g), u8(b));
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
static double delta_e76(const LabColor& a, const LabColor& b)
|
||||
{
|
||||
return std::sqrt(std::pow(a.l - b.l, 2.0) + std::pow(a.a - b.a, 2.0) + std::pow(a.b - b.b, 2.0));
|
||||
}
|
||||
|
||||
static bool material_matches(const std::string& a, const std::string& b)
|
||||
{
|
||||
if (a.empty() || b.empty())
|
||||
return false;
|
||||
return a == b || a == b + " Basic" || b == a + " Basic";
|
||||
}
|
||||
|
||||
static std::vector<std::vector<int>> ratio_grid(size_t n)
|
||||
{
|
||||
std::vector<std::vector<int>> out;
|
||||
if (n == 2) {
|
||||
for (int a = 20; a <= 80; a += 5)
|
||||
out.push_back({a, 100 - a});
|
||||
} else if (n == 3) {
|
||||
for (int a = 20; a <= 60; a += 5)
|
||||
for (int b = 20; b <= 80 - a; b += 5) {
|
||||
const int c = 100 - a - b;
|
||||
if (c >= 20)
|
||||
out.push_back({a, b, c});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static ColorDecomposeRecipeMode parse_mode(const std::string& s)
|
||||
{
|
||||
if (s == "RYBW" || s == "RGBY")
|
||||
return ColorDecomposeRecipeMode::RYBW;
|
||||
return ColorDecomposeRecipeMode::CMYW;
|
||||
}
|
||||
|
||||
static std::vector<StandardRecipeEntry> load_standard_entries()
|
||||
{
|
||||
std::vector<StandardRecipeEntry> entries;
|
||||
const std::string path = resources_dir() + "/filament_mixing/standard_color_recipes.json";
|
||||
std::ifstream ifs(path);
|
||||
if (!ifs)
|
||||
return entries;
|
||||
|
||||
nlohmann::json root = nlohmann::json::parse(ifs, nullptr, false);
|
||||
if (root.is_discarded() || !root.contains("entries") || !root["entries"].is_array())
|
||||
return entries;
|
||||
|
||||
for (const auto& item : root["entries"]) {
|
||||
if (!item.is_object())
|
||||
continue;
|
||||
StandardRecipeEntry entry;
|
||||
entry.mode = parse_mode(item.value("mode", "CMYW"));
|
||||
entry.material = item.value("material", "");
|
||||
entry.source = item.value("source", "");
|
||||
entry.measured_hex = item.value("measured_rgb", "");
|
||||
|
||||
if (item.contains("components") && item["components"].is_array()) {
|
||||
for (const auto& comp : item["components"]) {
|
||||
if (comp.is_object()) {
|
||||
entry.component_keys.push_back(comp.value("key", ""));
|
||||
entry.component_hexes.push_back(comp.value("rgb", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.contains("ratios") && item["ratios"].is_array()) {
|
||||
for (const auto& ratio : item["ratios"]) {
|
||||
if (ratio.is_number_integer())
|
||||
entry.ratios.push_back(ratio.get<int>());
|
||||
}
|
||||
}
|
||||
if (item.contains("measured_lab") && item["measured_lab"].is_array() && item["measured_lab"].size() >= 3) {
|
||||
entry.measured_lab = {
|
||||
item["measured_lab"][0].get<double>(),
|
||||
item["measured_lab"][1].get<double>(),
|
||||
item["measured_lab"][2].get<double>()
|
||||
};
|
||||
} else {
|
||||
ColorDecomposeRgb measured_rgb;
|
||||
if (!color_decompose_hex_to_rgb(entry.measured_hex, measured_rgb))
|
||||
continue;
|
||||
entry.measured_lab = rgb_to_lab(measured_rgb);
|
||||
}
|
||||
|
||||
if (entry.component_hexes.size() >= 2 && entry.component_hexes.size() == entry.ratios.size() &&
|
||||
!entry.measured_hex.empty())
|
||||
entries.push_back(std::move(entry));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
static const std::vector<StandardRecipeEntry>& standard_entries()
|
||||
{
|
||||
static const std::vector<StandardRecipeEntry> entries = load_standard_entries();
|
||||
return entries;
|
||||
}
|
||||
|
||||
static void evaluate_candidate(const ColorDecomposeRgb& target,
|
||||
const std::vector<std::string>& hexes,
|
||||
const std::vector<int>& ratios,
|
||||
const std::vector<unsigned int>& indices,
|
||||
ColorDecomposeRecipeMode mode,
|
||||
double& best_score,
|
||||
ColorDecomposeRecipeResult& best)
|
||||
{
|
||||
const std::string mixed = blend_color_multi(hexes, ratios);
|
||||
ColorDecomposeRgb mixed_rgb;
|
||||
if (!color_decompose_hex_to_rgb(mixed, mixed_rgb))
|
||||
return;
|
||||
|
||||
const double score = delta_e76(rgb_to_lab(target), rgb_to_lab(mixed_rgb));
|
||||
if (score >= best_score)
|
||||
return;
|
||||
|
||||
best_score = score;
|
||||
best.valid = true;
|
||||
best.mode = mode;
|
||||
best.matched_color_hex = mixed;
|
||||
best.components.clear();
|
||||
for (size_t i = 0; i < hexes.size(); ++i) {
|
||||
ColorDecomposeRecipeComponent comp;
|
||||
comp.color_hex = hexes[i];
|
||||
comp.ratio = ratios[i];
|
||||
comp.filament_index = i < indices.size() ? indices[i] : 0;
|
||||
best.components.push_back(comp);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb)
|
||||
{
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out)
|
||||
{
|
||||
if (hex.size() < 7 || hex[0] != '#')
|
||||
return false;
|
||||
unsigned r = 0, g = 0, b = 0;
|
||||
if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &r, &g, &b) != 3)
|
||||
return false;
|
||||
out = {static_cast<unsigned char>(r), static_cast<unsigned char>(g), static_cast<unsigned char>(b)};
|
||||
return true;
|
||||
}
|
||||
|
||||
ColorDecomposeRecipeResult recommend_from_physical_filaments(
|
||||
const ColorDecomposeRgb& target,
|
||||
const std::vector<ColorDecomposePhysicalFilament>& physical_filaments,
|
||||
const std::string& preferred_material_type)
|
||||
{
|
||||
std::vector<ColorDecomposePhysicalFilament> candidates;
|
||||
for (const auto& filament : physical_filaments) {
|
||||
if (filament.is_mixed)
|
||||
continue;
|
||||
ColorDecomposeRgb ignored;
|
||||
if (!color_decompose_hex_to_rgb(filament.color_hex, ignored))
|
||||
continue;
|
||||
if (preferred_material_type.empty() || material_matches(filament.type, preferred_material_type))
|
||||
candidates.push_back(filament);
|
||||
}
|
||||
|
||||
// Early exit: if a material-matched candidate has the exact target color,
|
||||
// return it as 100%. Downstream rejects single-component results (no mixed
|
||||
// slot created), which is correct -- the color already exists.
|
||||
const std::string target_hex = color_decompose_rgb_to_hex(target);
|
||||
for (const auto& cand : candidates) {
|
||||
ColorDecomposeRgb cand_rgb;
|
||||
if (!color_decompose_hex_to_rgb(cand.color_hex, cand_rgb))
|
||||
continue;
|
||||
if (color_decompose_rgb_to_hex(cand_rgb) == target_hex) {
|
||||
ColorDecomposeRecipeResult exact;
|
||||
exact.valid = true;
|
||||
exact.mode = ColorDecomposeRecipeMode::MaterialList;
|
||||
exact.matched_color_hex = cand.color_hex;
|
||||
ColorDecomposeRecipeComponent comp;
|
||||
comp.color_hex = cand.color_hex;
|
||||
comp.ratio = 100;
|
||||
comp.filament_index = cand.filament_index;
|
||||
exact.components.push_back(comp);
|
||||
return exact;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size() < 2)
|
||||
candidates = physical_filaments;
|
||||
candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [](const auto& filament) {
|
||||
if (filament.is_mixed)
|
||||
return true;
|
||||
ColorDecomposeRgb ignored;
|
||||
return !color_decompose_hex_to_rgb(filament.color_hex, ignored);
|
||||
}), candidates.end());
|
||||
|
||||
constexpr size_t kMaxCandidates = 8;
|
||||
if (candidates.size() > kMaxCandidates) {
|
||||
const LabColor target_lab = rgb_to_lab(target);
|
||||
std::sort(candidates.begin(), candidates.end(),
|
||||
[&target_lab](const ColorDecomposePhysicalFilament& a, const ColorDecomposePhysicalFilament& b) {
|
||||
ColorDecomposeRgb rgb_a, rgb_b;
|
||||
color_decompose_hex_to_rgb(a.color_hex, rgb_a);
|
||||
color_decompose_hex_to_rgb(b.color_hex, rgb_b);
|
||||
return delta_e76(target_lab, rgb_to_lab(rgb_a))
|
||||
< delta_e76(target_lab, rgb_to_lab(rgb_b));
|
||||
});
|
||||
candidates.resize(kMaxCandidates);
|
||||
}
|
||||
|
||||
ColorDecomposeRecipeResult best;
|
||||
double best_score = std::numeric_limits<double>::max();
|
||||
|
||||
for (size_t i = 0; i < candidates.size(); ++i) {
|
||||
for (size_t j = i + 1; j < candidates.size(); ++j) {
|
||||
const std::vector<std::string> hexes = {candidates[i].color_hex, candidates[j].color_hex};
|
||||
const std::vector<unsigned int> indices = {candidates[i].filament_index, candidates[j].filament_index};
|
||||
for (const auto& ratios : ratio_grid(2))
|
||||
evaluate_candidate(target, hexes, ratios, indices, ColorDecomposeRecipeMode::MaterialList, best_score, best);
|
||||
|
||||
for (size_t k = j + 1; k < candidates.size(); ++k) {
|
||||
const std::vector<std::string> hexes3 = {candidates[i].color_hex, candidates[j].color_hex, candidates[k].color_hex};
|
||||
const std::vector<unsigned int> indices3 = {candidates[i].filament_index, candidates[j].filament_index, candidates[k].filament_index};
|
||||
for (const auto& ratios : ratio_grid(3))
|
||||
evaluate_candidate(target, hexes3, ratios, indices3, ColorDecomposeRecipeMode::MaterialList, best_score, best);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
ColorDecomposeRecipeResult lookup_standard_recipe(
|
||||
const ColorDecomposeRgb& target,
|
||||
ColorDecomposeRecipeMode mode,
|
||||
const std::string& preferred_material_type)
|
||||
{
|
||||
const LabColor target_lab = rgb_to_lab(target);
|
||||
ColorDecomposeRecipeResult best;
|
||||
double best_score = std::numeric_limits<double>::max();
|
||||
|
||||
auto consider = [&](bool require_material_match) {
|
||||
for (const StandardRecipeEntry& entry : standard_entries()) {
|
||||
if (entry.mode != mode)
|
||||
continue;
|
||||
if (require_material_match && !material_matches(entry.material, preferred_material_type))
|
||||
continue;
|
||||
if (!require_material_match && !preferred_material_type.empty() && material_matches(entry.material, preferred_material_type))
|
||||
continue;
|
||||
|
||||
const double score = delta_e76(target_lab, entry.measured_lab);
|
||||
if (score >= best_score)
|
||||
continue;
|
||||
|
||||
best_score = score;
|
||||
best.valid = true;
|
||||
best.mode = mode;
|
||||
best.matched_color_hex = entry.measured_hex;
|
||||
best.components.clear();
|
||||
for (size_t i = 0; i < entry.component_hexes.size(); ++i) {
|
||||
ColorDecomposeRecipeComponent comp;
|
||||
comp.color_hex = entry.component_hexes[i];
|
||||
comp.base_color = i < entry.component_keys.size() ? entry.component_keys[i] : "";
|
||||
comp.ratio = entry.ratios[i];
|
||||
comp.filament_index = 0;
|
||||
best.components.push_back(comp);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
consider(true);
|
||||
if (!best.valid)
|
||||
consider(false);
|
||||
return best;
|
||||
}
|
||||
|
||||
std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes,
|
||||
const std::vector<int>& ratios)
|
||||
{
|
||||
if (component_hexes.size() < 2 || component_hexes.size() != ratios.size())
|
||||
return {};
|
||||
|
||||
auto normalize_hex = [](const std::string& hex) -> std::string {
|
||||
ColorDecomposeRgb rgb;
|
||||
if (!color_decompose_hex_to_rgb(hex, rgb))
|
||||
return {};
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
|
||||
return std::string(buf);
|
||||
};
|
||||
|
||||
// Stage 1: canonicalize input by sorting (hex, ratio) pairs so matching
|
||||
// is independent of the caller's component order.
|
||||
const size_t n = component_hexes.size();
|
||||
std::vector<std::pair<std::string, int>> in_pairs;
|
||||
in_pairs.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
std::string nh = normalize_hex(component_hexes[i]);
|
||||
if (nh.empty())
|
||||
return {};
|
||||
in_pairs.emplace_back(std::move(nh), ratios[i]);
|
||||
}
|
||||
std::sort(in_pairs.begin(), in_pairs.end());
|
||||
|
||||
std::vector<std::string> in_hexes;
|
||||
std::vector<int> in_ratios;
|
||||
in_hexes.reserve(n);
|
||||
in_ratios.reserve(n);
|
||||
for (const auto& p : in_pairs) {
|
||||
in_hexes.push_back(p.first);
|
||||
in_ratios.push_back(p.second);
|
||||
}
|
||||
|
||||
// Normalize ratios to sum=100 (callers may pass arbitrary weights,
|
||||
// e.g. MixedFilamentDialog uses ratio*10000).
|
||||
{
|
||||
int sum = 0;
|
||||
for (int r : in_ratios) sum += r;
|
||||
if (sum > 0 && sum != 100) {
|
||||
int new_sum = 0;
|
||||
for (size_t i = 0; i < in_ratios.size(); ++i) {
|
||||
in_ratios[i] = static_cast<int>(std::lround(
|
||||
static_cast<double>(in_ratios[i]) * 100.0 / static_cast<double>(sum)));
|
||||
new_sum += in_ratios[i];
|
||||
}
|
||||
if (new_sum != 100) {
|
||||
auto it = std::max_element(in_ratios.begin(), in_ratios.end());
|
||||
*it += (100 - new_sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to polynomial model for ratios outside the measured range.
|
||||
{
|
||||
bool out_of_range = false;
|
||||
if (n == 2) {
|
||||
for (int r : in_ratios)
|
||||
if (r < 20 || r > 80) { out_of_range = true; break; }
|
||||
} else {
|
||||
for (int r : in_ratios)
|
||||
if (r < 20) { out_of_range = true; break; }
|
||||
}
|
||||
if (out_of_range)
|
||||
return {};
|
||||
}
|
||||
|
||||
// Stage 2: collect anchors with the same component hex set; try exact match.
|
||||
struct Anchor {
|
||||
std::vector<int> ratios;
|
||||
LabColor lab;
|
||||
std::string hex;
|
||||
};
|
||||
std::vector<Anchor> anchors;
|
||||
|
||||
for (const StandardRecipeEntry& entry : standard_entries()) {
|
||||
if (entry.source != "measured" && entry.source != "interpolated")
|
||||
continue;
|
||||
if (entry.component_hexes.size() != n)
|
||||
continue;
|
||||
|
||||
std::vector<std::pair<std::string, int>> e_pairs;
|
||||
e_pairs.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
e_pairs.emplace_back(normalize_hex(entry.component_hexes[i]), entry.ratios[i]);
|
||||
std::sort(e_pairs.begin(), e_pairs.end());
|
||||
|
||||
bool same_set = true;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (e_pairs[i].first != in_hexes[i]) { same_set = false; break; }
|
||||
if (!same_set)
|
||||
continue;
|
||||
|
||||
Anchor a;
|
||||
a.ratios.reserve(n);
|
||||
for (const auto& p : e_pairs) a.ratios.push_back(p.second);
|
||||
a.lab = entry.measured_lab;
|
||||
a.hex = entry.measured_hex;
|
||||
|
||||
if (a.ratios == in_ratios)
|
||||
return a.hex;
|
||||
|
||||
anchors.push_back(std::move(a));
|
||||
}
|
||||
|
||||
if (anchors.size() < 2)
|
||||
return {};
|
||||
|
||||
// Stage 3: interpolation in Lab space.
|
||||
if (n == 2) {
|
||||
// 1D linear interpolation along ratio[0].
|
||||
std::sort(anchors.begin(), anchors.end(),
|
||||
[](const Anchor& a, const Anchor& b) { return a.ratios[0] < b.ratios[0]; });
|
||||
const double x = static_cast<double>(in_ratios[0]);
|
||||
size_t lo = 0;
|
||||
while (lo + 2 < anchors.size() && static_cast<double>(anchors[lo + 1].ratios[0]) <= x)
|
||||
++lo;
|
||||
const Anchor& a0 = anchors[lo];
|
||||
const Anchor& a1 = anchors[lo + 1];
|
||||
const double span = static_cast<double>(a1.ratios[0] - a0.ratios[0]);
|
||||
const double t = span > 0.0 ? (x - static_cast<double>(a0.ratios[0])) / span : 0.0;
|
||||
return lab_to_srgb_hex({a0.lab.l + t * (a1.lab.l - a0.lab.l),
|
||||
a0.lab.a + t * (a1.lab.a - a0.lab.a),
|
||||
a0.lab.b + t * (a1.lab.b - a0.lab.b)});
|
||||
}
|
||||
|
||||
// 3+ color: IDW (p=2) with 3 nearest anchors in the (ratio[0], ratio[1]) plane.
|
||||
const double ra = static_cast<double>(in_ratios[0]);
|
||||
const double rb = static_cast<double>(in_ratios[1]);
|
||||
std::vector<std::pair<double, const Anchor*>> dists;
|
||||
dists.reserve(anchors.size());
|
||||
for (const Anchor& a : anchors) {
|
||||
const double d = std::sqrt(std::pow(ra - static_cast<double>(a.ratios[0]), 2.0) +
|
||||
std::pow(rb - static_cast<double>(a.ratios[1]), 2.0));
|
||||
if (d == 0.0)
|
||||
return a.hex;
|
||||
dists.emplace_back(d, &a);
|
||||
}
|
||||
const size_t k = std::min(static_cast<size_t>(3), dists.size());
|
||||
std::partial_sort(dists.begin(), dists.begin() + k, dists.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
double num_l = 0.0, num_a = 0.0, num_b = 0.0, den = 0.0;
|
||||
for (size_t j = 0; j < k; ++j) {
|
||||
const double w = 1.0 / (dists[j].first * dists[j].first);
|
||||
num_l += w * dists[j].second->lab.l;
|
||||
num_a += w * dists[j].second->lab.a;
|
||||
num_b += w * dists[j].second->lab.b;
|
||||
den += w;
|
||||
}
|
||||
return lab_to_srgb_hex({num_l / den, num_a / den, num_b / den});
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -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
|
||||
@@ -0,0 +1,829 @@
|
||||
#include "FilamentMixer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <numeric>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "ColorDecomposeRecipe.hpp"
|
||||
#include "FilamentMixerModel.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
inline float clamp01(float x)
|
||||
{
|
||||
return std::max(0.0f, std::min(1.0f, x));
|
||||
}
|
||||
|
||||
inline float srgb_to_linear(float x)
|
||||
{
|
||||
return (x >= 0.04045f) ? std::pow((x + 0.055f) / 1.055f, 2.4f) : x / 12.92f;
|
||||
}
|
||||
|
||||
inline float linear_to_srgb(float x)
|
||||
{
|
||||
return (x >= 0.0031308f) ? (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f) : (12.92f * x);
|
||||
}
|
||||
|
||||
inline unsigned char to_u8(float x)
|
||||
{
|
||||
const float clamped = clamp01(x);
|
||||
return static_cast<unsigned char>(clamped * 255.0f + 0.5f);
|
||||
}
|
||||
|
||||
inline float to_f01(unsigned char x)
|
||||
{
|
||||
return static_cast<float>(x) / 255.0f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
|
||||
unsigned char r2, unsigned char g2, unsigned char b2,
|
||||
float t,
|
||||
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b)
|
||||
{
|
||||
::filament_mixer::lerp(r1, g1, b1, r2, g2, b2, t, out_r, out_g, out_b);
|
||||
}
|
||||
|
||||
void filament_mixer_lerp_float(float r1, float g1, float b1,
|
||||
float r2, float g2, float b2,
|
||||
float t,
|
||||
float* out_r, float* out_g, float* out_b)
|
||||
{
|
||||
unsigned char ur = 0, ug = 0, ub = 0;
|
||||
filament_mixer_lerp(to_u8(r1), to_u8(g1), to_u8(b1),
|
||||
to_u8(r2), to_u8(g2), to_u8(b2),
|
||||
t, &ur, &ug, &ub);
|
||||
*out_r = to_f01(ur);
|
||||
*out_g = to_f01(ug);
|
||||
*out_b = to_f01(ub);
|
||||
}
|
||||
|
||||
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
|
||||
float r2, float g2, float b2,
|
||||
float t,
|
||||
float* out_r, float* out_g, float* out_b)
|
||||
{
|
||||
const float sr1 = linear_to_srgb(clamp01(r1));
|
||||
const float sg1 = linear_to_srgb(clamp01(g1));
|
||||
const float sb1 = linear_to_srgb(clamp01(b1));
|
||||
const float sr2 = linear_to_srgb(clamp01(r2));
|
||||
const float sg2 = linear_to_srgb(clamp01(g2));
|
||||
const float sb2 = linear_to_srgb(clamp01(b2));
|
||||
|
||||
float out_sr = 0.0f, out_sg = 0.0f, out_sb = 0.0f;
|
||||
filament_mixer_lerp_float(sr1, sg1, sb1, sr2, sg2, sb2, t, &out_sr, &out_sg, &out_sb);
|
||||
|
||||
*out_r = srgb_to_linear(clamp01(out_sr));
|
||||
*out_g = srgb_to_linear(clamp01(out_sg));
|
||||
*out_b = srgb_to_linear(clamp01(out_sb));
|
||||
}
|
||||
|
||||
static bool parse_hex(const std::string &hex, unsigned char &r, unsigned char &g, unsigned char &b)
|
||||
{
|
||||
if (hex.size() < 7 || hex[0] != '#') return false;
|
||||
unsigned rv = 0, gv = 0, bv = 0;
|
||||
if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &rv, &gv, &bv) != 3) return false;
|
||||
r = (unsigned char)rv; g = (unsigned char)gv; b = (unsigned char)bv;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b)
|
||||
{
|
||||
unsigned char r1 = 128, g1 = 128, b1 = 128;
|
||||
unsigned char r2 = 128, g2 = 128, b2 = 128;
|
||||
parse_hex(hex_a, r1, g1, b1);
|
||||
parse_hex(hex_b, r2, g2, b2);
|
||||
|
||||
unsigned char mr = 0, mg = 0, mb = 0;
|
||||
filament_mixer_lerp(r1, g1, b1, r2, g2, b2, ratio_b, &mr, &mg, &mb);
|
||||
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", mr, mg, mb);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string blend_color_multi(const std::vector<std::string> &hex_colors,
|
||||
const std::vector<int> &weights)
|
||||
{
|
||||
if (hex_colors.size() >= 2 && hex_colors.size() == weights.size()) {
|
||||
std::string measured = lookup_measured_blend_color(hex_colors, weights);
|
||||
if (!measured.empty())
|
||||
return measured;
|
||||
}
|
||||
|
||||
if (hex_colors.empty())
|
||||
return "#000000";
|
||||
if (hex_colors.size() == 1) {
|
||||
unsigned char cr = 128, cg = 128, cb = 128;
|
||||
parse_hex(hex_colors.front(), cr, cg, cb);
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", cr, cg, cb);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
assert(hex_colors.size() == weights.size());
|
||||
|
||||
unsigned char r = 128, g = 128, b = 128;
|
||||
int accumulated = 0;
|
||||
|
||||
for (size_t i = 0; i < hex_colors.size() && i < weights.size(); ++i) {
|
||||
if (weights[i] <= 0)
|
||||
continue;
|
||||
unsigned char cr = 128, cg = 128, cb = 128;
|
||||
parse_hex(hex_colors[i], cr, cg, cb);
|
||||
if (accumulated == 0) {
|
||||
r = cr; g = cg; b = cb;
|
||||
accumulated = weights[i];
|
||||
} else {
|
||||
const int new_total = accumulated + weights[i];
|
||||
const float t = static_cast<float>(weights[i]) / static_cast<float>(new_total);
|
||||
filament_mixer_lerp(r, g, b, cr, cg, cb, t, &r, &g, &b);
|
||||
accumulated = new_total;
|
||||
}
|
||||
}
|
||||
|
||||
if (accumulated == 0)
|
||||
return "#000000";
|
||||
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::vector<unsigned int> parse_mixed_components(const std::string &str)
|
||||
{
|
||||
std::vector<unsigned int> components;
|
||||
if (str.empty())
|
||||
return components;
|
||||
std::istringstream ss(str);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
try {
|
||||
int val = std::stoi(token);
|
||||
if (val >= 0)
|
||||
components.push_back(static_cast<unsigned int>(val));
|
||||
} catch (...) {}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Parse a token that may represent a finite double or "use default" (empty / "nan").
|
||||
// Returns NaN on either explicit sentinel or any parse error.
|
||||
inline double parse_tangent_token(const std::string& tok)
|
||||
{
|
||||
if (tok.empty()) return std::numeric_limits<double>::quiet_NaN();
|
||||
std::string lower(tok.size(), '\0');
|
||||
std::transform(tok.begin(), tok.end(), lower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lower == "nan") return std::numeric_limits<double>::quiet_NaN();
|
||||
try {
|
||||
const double v = std::stod(tok);
|
||||
if (!std::isfinite(v)) return std::numeric_limits<double>::quiet_NaN();
|
||||
return v;
|
||||
} catch (...) {
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
}
|
||||
|
||||
// Split a "a,b,c,d" segment on commas, preserving empty tokens (so "0.5,0.4,," yields
|
||||
// {"0.5","0.4","",""}). Used by the gradient-curve parser to distinguish NaN tangents
|
||||
// from a malformed segment.
|
||||
inline std::vector<std::string> split_commas(const std::string& seg)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
size_t start = 0;
|
||||
while (true) {
|
||||
const size_t comma = seg.find(',', start);
|
||||
if (comma == std::string::npos) {
|
||||
out.emplace_back(seg.substr(start));
|
||||
return out;
|
||||
}
|
||||
out.emplace_back(seg.substr(start, comma - start));
|
||||
start = comma + 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Default Fritsch-Carlson PCHIP tangents for a sorted-by-x anchor list. m has size n
|
||||
// matching the anchor count; for n == 1 the tangent is 0; for n == 2 both endpoint
|
||||
// tangents equal the single secant (degenerates to linear).
|
||||
std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts)
|
||||
{
|
||||
const size_t n = pts.size();
|
||||
std::vector<double> m(n, 0.0);
|
||||
if (n < 2) return m;
|
||||
|
||||
std::vector<double> d(n - 1);
|
||||
for (size_t i = 0; i + 1 < n; ++i) {
|
||||
const double h = std::max(1e-12, pts[i + 1].x - pts[i].x);
|
||||
d[i] = (pts[i + 1].y - pts[i].y) / h;
|
||||
}
|
||||
|
||||
m[0] = d[0];
|
||||
m[n - 1] = d[n - 2];
|
||||
for (size_t i = 1; i + 1 < n; ++i)
|
||||
m[i] = 0.5 * (d[i - 1] + d[i]);
|
||||
|
||||
// Fritsch-Carlson monotonic guard: kill flats then rescale steep tangents so the
|
||||
// resulting cubic never overshoots [min, max] of the surrounding anchors.
|
||||
for (size_t i = 0; i + 1 < n; ++i) {
|
||||
if (d[i] == 0.0) {
|
||||
m[i] = 0.0;
|
||||
m[i + 1] = 0.0;
|
||||
continue;
|
||||
}
|
||||
const double a = m[i] / d[i];
|
||||
const double b = m[i + 1] / d[i];
|
||||
const double s = a * a + b * b;
|
||||
if (s > 9.0) {
|
||||
const double tau = 3.0 / std::sqrt(s);
|
||||
m[i] = tau * a * d[i];
|
||||
m[i + 1] = tau * b * d[i];
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
GradientCurve parse_gradient_curve(const std::string& s)
|
||||
{
|
||||
GradientCurve curve;
|
||||
if (s.empty())
|
||||
return curve;
|
||||
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
std::istringstream ss(s);
|
||||
std::string segment;
|
||||
while (std::getline(ss, segment, '|')) {
|
||||
if (segment.empty())
|
||||
continue;
|
||||
const auto fields = split_commas(segment);
|
||||
// 2-field legacy form -> (x, y), tangents stay NaN.
|
||||
// 4-field form -> (x, y, m_in, m_out), empty / "nan" tokens preserved as NaN.
|
||||
if (fields.size() != 2 && fields.size() != 4) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring malformed segment \""
|
||||
<< segment << "\" (expected 2 or 4 comma-separated fields, got "
|
||||
<< fields.size() << ")";
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
double x = std::stod(fields[0]);
|
||||
double y = std::stod(fields[1]);
|
||||
x = std::max(0.0, std::min(1.0, x));
|
||||
y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, y));
|
||||
GradientAnchor a;
|
||||
a.x = x;
|
||||
a.y = y;
|
||||
if (fields.size() == 4) {
|
||||
a.m_in = parse_tangent_token(fields[2]);
|
||||
a.m_out = parse_tangent_token(fields[3]);
|
||||
}
|
||||
curve.points.push_back(a);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring unparseable segment \""
|
||||
<< segment << "\": " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
if (curve.points.size() < 2) {
|
||||
if (!curve.points.empty())
|
||||
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: only "
|
||||
<< curve.points.size() << " valid point(s), need at least 2; discarding";
|
||||
curve.points.clear();
|
||||
return curve;
|
||||
}
|
||||
|
||||
std::sort(curve.points.begin(), curve.points.end(),
|
||||
[](const GradientAnchor& a, const GradientAnchor& b) {
|
||||
return a.x < b.x;
|
||||
});
|
||||
return curve;
|
||||
}
|
||||
|
||||
std::string serialize_gradient_curve(const GradientCurve& c)
|
||||
{
|
||||
if (c.points.empty())
|
||||
return std::string{};
|
||||
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
std::string out;
|
||||
char buf[128];
|
||||
for (size_t i = 0; i < c.points.size(); ++i) {
|
||||
if (i > 0) out += '|';
|
||||
const auto& a = c.points[i];
|
||||
const bool has_in = std::isfinite(a.m_in);
|
||||
const bool has_out = std::isfinite(a.m_out);
|
||||
if (has_in || has_out) {
|
||||
// Emit empty tokens for NaN slots so the legacy parser would still split
|
||||
// four fields; the new parser interprets empty tokens as "use PCHIP default".
|
||||
char in_buf[32] = {0};
|
||||
char out_buf[32] = {0};
|
||||
if (has_in) std::snprintf(in_buf, sizeof(in_buf), "%.4f", a.m_in);
|
||||
if (has_out) std::snprintf(out_buf, sizeof(out_buf), "%.4f", a.m_out);
|
||||
std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%s,%s",
|
||||
a.x, a.y, in_buf, out_buf);
|
||||
} else {
|
||||
// 4-field form is only emitted when at least one tangent is finite; the
|
||||
// 2-field form is emitted otherwise so the JSON payload stays minimal
|
||||
// and remains readable by older clients that only know (x, y) pairs.
|
||||
std::snprintf(buf, sizeof(buf), "%.4f,%.4f", a.x, a.y);
|
||||
}
|
||||
out += buf;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
double sample_gradient_curve(const GradientCurve& c, double t)
|
||||
{
|
||||
const auto& pts = c.points;
|
||||
if (pts.size() < 2)
|
||||
return 0.5;
|
||||
if (t <= pts.front().x)
|
||||
return pts.front().y;
|
||||
if (t >= pts.back().x)
|
||||
return pts.back().y;
|
||||
|
||||
// PCHIP defaults are computed for every call; control point counts are typically
|
||||
// tiny (< 16) so the allocation cost is negligible compared to any actual rendering
|
||||
// or G-code work that drives the sampler.
|
||||
const std::vector<double> m_def = compute_pchip_default_tangents(pts);
|
||||
const size_t n = pts.size();
|
||||
|
||||
// Linear scan to locate the interval [pts[i].x, pts[i+1].x] containing t. Cheap
|
||||
// and avoids the upper_bound boilerplate; n is small.
|
||||
for (size_t i = 1; i < n; ++i) {
|
||||
const double x0 = pts[i - 1].x;
|
||||
const double x1 = pts[i].x;
|
||||
if (t > x1) continue;
|
||||
|
||||
const double y0 = pts[i - 1].y;
|
||||
const double y1 = pts[i].y;
|
||||
const double h = std::max(1e-12, x1 - x0);
|
||||
const double m_left = std::isfinite(pts[i - 1].m_out) ? pts[i - 1].m_out : m_def[i - 1];
|
||||
const double m_right = std::isfinite(pts[i].m_in) ? pts[i].m_in : m_def[i];
|
||||
|
||||
const double u = (t - x0) / h;
|
||||
const double u2 = u * u;
|
||||
const double u3 = u2 * u;
|
||||
const double h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
|
||||
const double h10 = u3 - 2.0 * u2 + u;
|
||||
const double h01 = -2.0 * u3 + 3.0 * u2;
|
||||
const double h11 = u3 - u2;
|
||||
double y = h00 * y0 + h10 * h * m_left
|
||||
+ h01 * y1 + h11 * h * m_right;
|
||||
// Defensive clamp in case tangent overrides on legacy curves push the
|
||||
// single-segment Hermite slightly outside the anchor band.
|
||||
if (y < kGradientMinRatio) y = kGradientMinRatio;
|
||||
if (y > kGradientMaxRatio) y = kGradientMaxRatio;
|
||||
return y;
|
||||
}
|
||||
return pts.back().y;
|
||||
}
|
||||
|
||||
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components)
|
||||
{
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
std::vector<double> ratios;
|
||||
if (!str.empty()) {
|
||||
std::istringstream ss(str);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
try {
|
||||
double val = std::stod(token);
|
||||
if (val > 0.0)
|
||||
ratios.push_back(val);
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (ratios.size() != n_components || n_components == 0) {
|
||||
ratios.assign(n_components, n_components > 0 ? 1.0 / n_components : 0.0);
|
||||
return ratios;
|
||||
}
|
||||
|
||||
double sum = std::accumulate(ratios.begin(), ratios.end(), 0.0);
|
||||
if (sum > 0.0 && std::abs(sum - 1.0) > 1e-6) {
|
||||
for (double &r : ratios)
|
||||
r /= sum;
|
||||
}
|
||||
return ratios;
|
||||
}
|
||||
|
||||
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
|
||||
{
|
||||
for (unsigned char v : is_mixed)
|
||||
if (v) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<size_t> check_mixed_filament_integrity(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs,
|
||||
size_t num_physical)
|
||||
{
|
||||
std::vector<size_t> broken;
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i]) continue;
|
||||
if (i >= comp_strs.size() || comp_strs[i].empty()) {
|
||||
broken.push_back(i);
|
||||
continue;
|
||||
}
|
||||
auto comps = parse_mixed_components(comp_strs[i]);
|
||||
if (comps.size() < 2) {
|
||||
broken.push_back(i);
|
||||
continue;
|
||||
}
|
||||
for (unsigned int c : comps) {
|
||||
if (c < 1 || c > num_physical) {
|
||||
broken.push_back(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return broken;
|
||||
}
|
||||
|
||||
std::vector<unsigned int> expand_mixed_filaments(
|
||||
const std::vector<unsigned int> &extruders_0based,
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs)
|
||||
{
|
||||
std::vector<unsigned int> result;
|
||||
for (unsigned int ext : extruders_0based) {
|
||||
if (ext < is_mixed.size() && is_mixed[ext] && ext < comp_strs.size()) {
|
||||
auto comps = parse_mixed_components(comp_strs[ext]);
|
||||
for (unsigned int c : comps)
|
||||
if (c >= 1) result.push_back(c - 1);
|
||||
} else {
|
||||
result.push_back(ext);
|
||||
}
|
||||
}
|
||||
std::sort(result.begin(), result.end());
|
||||
result.erase(std::unique(result.begin(), result.end()), result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
void remap_mixed_components_on_delete(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
std::vector<std::string> &comp_strs,
|
||||
unsigned int del_1based)
|
||||
{
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i]) continue;
|
||||
if (i >= comp_strs.size() || comp_strs[i].empty()) continue;
|
||||
|
||||
auto comps = parse_mixed_components(comp_strs[i]);
|
||||
std::ostringstream ss;
|
||||
for (size_t j = 0; j < comps.size(); ++j) {
|
||||
if (j > 0) ss << ',';
|
||||
if (comps[j] == del_1based)
|
||||
ss << 0;
|
||||
else if (comps[j] > del_1based)
|
||||
ss << (comps[j] - 1);
|
||||
else
|
||||
ss << comps[j];
|
||||
}
|
||||
comp_strs[i] = ss.str();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<size_t> check_mixed_filament_type_consistency(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs,
|
||||
const std::vector<std::string> &filament_types)
|
||||
{
|
||||
std::vector<size_t> result;
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i]) continue;
|
||||
if (i >= comp_strs.size() || comp_strs[i].empty()) continue;
|
||||
auto comps = parse_mixed_components(comp_strs[i]);
|
||||
if (comps.size() < 2) continue;
|
||||
|
||||
std::string ref_type;
|
||||
bool mismatch = false;
|
||||
for (unsigned int c : comps) {
|
||||
if (c == 0) continue; // sentinel for deleted component
|
||||
size_t idx = static_cast<size_t>(c) - 1; // 1-based -> 0-based
|
||||
if (idx >= filament_types.size()) continue;
|
||||
if (ref_type.empty())
|
||||
ref_type = filament_types[idx];
|
||||
else if (filament_types[idx] != ref_type) {
|
||||
mismatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mismatch)
|
||||
result.push_back(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void expand_mixed_slots_in_unprintables(
|
||||
std::vector<std::set<int>> &unprintables,
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs)
|
||||
{
|
||||
for (auto &unprintable_set : unprintables) {
|
||||
std::set<int> expanded;
|
||||
for (int fid : unprintable_set) {
|
||||
if (fid >= 0 && (size_t)fid < is_mixed.size() && is_mixed[fid]
|
||||
&& (size_t)fid < comp_strs.size()) {
|
||||
auto comps = parse_mixed_components(comp_strs[fid]);
|
||||
for (unsigned int c : comps)
|
||||
if (c >= 1) expanded.insert((int)(c - 1));
|
||||
} else {
|
||||
expanded.insert(fid);
|
||||
}
|
||||
}
|
||||
unprintable_set = std::move(expanded);
|
||||
}
|
||||
}
|
||||
|
||||
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals)
|
||||
{
|
||||
for (size_t i = 0; i < vals.size(); ++i) {
|
||||
if (vals[i].empty())
|
||||
continue;
|
||||
// parse_gradient_curve returns empty for both "empty input" and "<2 valid points";
|
||||
// we already skipped empty, so an empty result means a corrupted single-point slot.
|
||||
if (parse_gradient_curve(vals[i]).empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "sanitize_mixed_gradient_curve_array: slot "
|
||||
<< i << " curve \"" << vals[i]
|
||||
<< "\" has fewer than 2 valid points; clearing to linear";
|
||||
vals[i].clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool try_parse_mixed_components_strict(const std::string &str,
|
||||
std::vector<unsigned int> &components,
|
||||
std::string &err)
|
||||
{
|
||||
components.clear();
|
||||
if (str.empty()) {
|
||||
err = "empty component list";
|
||||
return false;
|
||||
}
|
||||
std::istringstream ss(str);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
if (token.empty()) {
|
||||
err = "empty component index";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const long val = std::stol(token);
|
||||
if (val < 1) {
|
||||
err = "component index must be >= 1 (got " + token + ")";
|
||||
return false;
|
||||
}
|
||||
components.push_back(static_cast<unsigned int>(val));
|
||||
} catch (...) {
|
||||
err = "invalid component index \"" + token + "\"";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (components.size() < 2) {
|
||||
err = "at least 2 components required (got " + std::to_string(components.size()) + ")";
|
||||
return false;
|
||||
}
|
||||
std::set<unsigned int> seen;
|
||||
for (unsigned int c : components) {
|
||||
if (!seen.insert(c).second) {
|
||||
err = "duplicate component index " + std::to_string(c);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool try_parse_mixed_ratios_strict(const std::string &str,
|
||||
size_t n_components,
|
||||
std::string &err)
|
||||
{
|
||||
if (str.empty())
|
||||
return true;
|
||||
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
std::vector<double> ratios;
|
||||
std::istringstream ss(str);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
if (token.empty()) {
|
||||
err = "empty ratio value";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const double val = std::stod(token);
|
||||
if (!(val > 0.0)) {
|
||||
err = "ratio must be positive (got " + token + ")";
|
||||
return false;
|
||||
}
|
||||
ratios.push_back(val);
|
||||
} catch (...) {
|
||||
err = "invalid ratio \"" + token + "\"";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ratios.size() != n_components) {
|
||||
err = "expected " + std::to_string(n_components) + " ratio(s), got "
|
||||
+ std::to_string(ratios.size());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validate_gradient_range_strict(const std::string &str, std::string &err)
|
||||
{
|
||||
if (str.empty())
|
||||
return true;
|
||||
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
float v0 = 0.f, v1 = 0.f;
|
||||
if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) {
|
||||
err = "expected two comma-separated floats, e.g. \"0.10,0.90\"";
|
||||
return false;
|
||||
}
|
||||
if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) {
|
||||
err = "start and end ratios must be in (0, 1)";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void append_error(std::map<std::string, std::string> &errors,
|
||||
const std::string &key,
|
||||
const std::string &msg)
|
||||
{
|
||||
auto it = errors.find(key);
|
||||
if (it == errors.end())
|
||||
errors.emplace(key, msg);
|
||||
else
|
||||
it->second += "; " + msg;
|
||||
}
|
||||
|
||||
static bool has_mixed_sub_params_specified(
|
||||
const std::vector<std::string> &comp_strs,
|
||||
const std::vector<std::string> &ratio_strs,
|
||||
const std::vector<unsigned char> &gradient_flags)
|
||||
{
|
||||
for (const std::string &s : comp_strs)
|
||||
if (!s.empty()) return true;
|
||||
for (const std::string &s : ratio_strs)
|
||||
if (!s.empty()) return true;
|
||||
for (unsigned char g : gradient_flags)
|
||||
if (g) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool mixed_string_array_was_specified(const std::vector<std::string> &vals)
|
||||
{
|
||||
for (const std::string &s : vals)
|
||||
if (!s.empty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool mixed_bool_array_was_specified(const std::vector<unsigned char> &vals)
|
||||
{
|
||||
for (unsigned char v : vals)
|
||||
if (v)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void check_mixed_array_size_required(std::map<std::string, std::string> &errors,
|
||||
const std::string &opt_key,
|
||||
size_t actual_size,
|
||||
size_t expected_size)
|
||||
{
|
||||
if (actual_size != expected_size) {
|
||||
append_error(errors, opt_key,
|
||||
"array size " + std::to_string(actual_size)
|
||||
+ " does not match filament slot count " + std::to_string(expected_size));
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> validate_mixed_filament_params(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs,
|
||||
const std::vector<std::string> &ratio_strs,
|
||||
const std::vector<unsigned char> &gradient_flags,
|
||||
const std::vector<std::string> &gradient_range_strs,
|
||||
const std::vector<std::string> &gradient_curve_strs)
|
||||
{
|
||||
std::map<std::string, std::string> errors;
|
||||
|
||||
if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags)
|
||||
&& !has_any_mixed_filament(is_mixed)) {
|
||||
append_error(errors, "filament_is_mixed",
|
||||
"must be set when mixed filament parameters are specified");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!has_any_mixed_filament(is_mixed))
|
||||
return errors;
|
||||
|
||||
const size_t slot_count = is_mixed.size();
|
||||
|
||||
// Rule 1: mixed filament model → components & ratios arrays must cover every slot.
|
||||
check_mixed_array_size_required(errors, "filament_mixed_components", comp_strs.size(), slot_count);
|
||||
check_mixed_array_size_required(errors, "filament_mixed_sublayer_ratios", ratio_strs.size(), slot_count);
|
||||
|
||||
// Rule 2: gradient passed (any slot true) → gradient & range arrays must cover every slot.
|
||||
const bool gradient_specified = mixed_bool_array_was_specified(gradient_flags);
|
||||
if (gradient_specified) {
|
||||
check_mixed_array_size_required(errors, "filament_mixed_gradient", gradient_flags.size(), slot_count);
|
||||
check_mixed_array_size_required(errors, "filament_mixed_gradient_range", gradient_range_strs.size(), slot_count);
|
||||
}
|
||||
|
||||
// Rule 3: curve passed (any non-empty entry) → curve array must cover every slot.
|
||||
const bool curve_specified = mixed_string_array_was_specified(gradient_curve_strs);
|
||||
if (curve_specified)
|
||||
check_mixed_array_size_required(errors, "filament_mixed_gradient_curve", gradient_curve_strs.size(), slot_count);
|
||||
|
||||
size_t num_physical = 0;
|
||||
for (unsigned char v : is_mixed)
|
||||
if (!v) ++num_physical;
|
||||
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i])
|
||||
continue;
|
||||
|
||||
const std::string slot = "slot " + std::to_string(i + 1);
|
||||
const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : "";
|
||||
|
||||
std::vector<unsigned int> components;
|
||||
std::string comp_err;
|
||||
if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) {
|
||||
append_error(errors, "filament_mixed_components", slot + ": " + comp_err);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned int c : components) {
|
||||
if (c > num_physical) {
|
||||
append_error(errors, "filament_mixed_components",
|
||||
slot + ": component " + std::to_string(c)
|
||||
+ " out of range (max physical filament index is "
|
||||
+ std::to_string(num_physical) + ")");
|
||||
break;
|
||||
}
|
||||
if (c == i + 1) {
|
||||
append_error(errors, "filament_mixed_components",
|
||||
slot + ": cannot reference itself as a component");
|
||||
break;
|
||||
}
|
||||
const size_t idx0 = static_cast<size_t>(c - 1);
|
||||
if (idx0 < is_mixed.size() && is_mixed[idx0]) {
|
||||
append_error(errors, "filament_mixed_components",
|
||||
slot + ": component " + std::to_string(c)
|
||||
+ " references a mixed filament slot");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ratio_err;
|
||||
const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : "";
|
||||
if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err))
|
||||
append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err);
|
||||
|
||||
const bool gradient_on = i < gradient_flags.size() && gradient_flags[i];
|
||||
if (gradient_on) {
|
||||
if (components.size() != 2) {
|
||||
append_error(errors, "filament_mixed_gradient",
|
||||
slot + ": gradient requires exactly 2 components");
|
||||
}
|
||||
|
||||
if (gradient_specified) {
|
||||
std::string range_err;
|
||||
const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : "";
|
||||
if (!validate_gradient_range_strict(range_str, range_err))
|
||||
append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err);
|
||||
}
|
||||
|
||||
if (curve_specified) {
|
||||
const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : "";
|
||||
if (!curve_str.empty() && parse_gradient_curve(curve_str).empty())
|
||||
append_error(errors, "filament_mixed_gradient_curve",
|
||||
slot + ": invalid curve (need at least 2 valid control points)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,164 @@
|
||||
#ifndef SLIC3R_FILAMENT_MIXER_HPP
|
||||
#define SLIC3R_FILAMENT_MIXER_HPP
|
||||
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Photoshop-style gradient curve control point in [0,1] x [0,1].
|
||||
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
|
||||
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
|
||||
// anchors loaded from old 2-field 3MF projects or freshly added via a quick click.
|
||||
// A press-and-drag on a curve segment populates m_out of its left anchor and m_in of
|
||||
// its right anchor so the segment bends without inserting a new anchor.
|
||||
struct GradientAnchor {
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double m_in = std::numeric_limits<double>::quiet_NaN();
|
||||
double m_out = std::numeric_limits<double>::quiet_NaN();
|
||||
};
|
||||
|
||||
// Sorted list of GradientAnchor; x in [0,1], y in [kGradientMinRatio, kGradientMaxRatio].
|
||||
// Empty means "no custom curve" (callers should fall back to the linear range).
|
||||
struct GradientCurve {
|
||||
std::vector<GradientAnchor> points;
|
||||
bool empty() const { return points.empty(); }
|
||||
};
|
||||
|
||||
// Reserved blend ratio range. Anchor y values (= component 0's ratio) are constrained
|
||||
// to this band so the mixed filament never reaches pure 0% / 100% of either physical
|
||||
// component, which keeps both extruders flowing and avoids degenerate transitions.
|
||||
// Both the editor and the sampler enforce this clamp.
|
||||
constexpr double kGradientMinRatio = 0.1;
|
||||
constexpr double kGradientMaxRatio = 0.9;
|
||||
|
||||
// Parse "x0,y0[,m_in0,m_out0]|x1,y1[,m_in1,m_out1]|..." into a GradientCurve.
|
||||
// (Anchors are pipe-separated; the fields within an anchor are comma-separated.)
|
||||
// Accepts both the legacy 2-field form (tangents -> NaN) and the new 4-field form
|
||||
// (empty token or "nan" preserved as NaN). Returns an empty curve when the input is
|
||||
// empty or unparsable. Points are clamped to [0,1] for (x, y) and re-sorted by x.
|
||||
GradientCurve parse_gradient_curve(const std::string& s);
|
||||
|
||||
// Serialize a GradientCurve back to a string. Emits 4 fields per anchor when any
|
||||
// tangent override is finite; emits 2 fields when both tangents are NaN so unchanged
|
||||
// projects stay byte-identical with the legacy format. Returns "" when empty.
|
||||
std::string serialize_gradient_curve(const GradientCurve& c);
|
||||
|
||||
// Sample the curve at t in [0,1] using cubic Hermite with Fritsch-Carlson PCHIP
|
||||
// default tangents, optionally overridden per anchor via m_in / m_out. Returns the
|
||||
// clamped end values when t is outside the control point range. Returns 0.5 when the
|
||||
// curve has fewer than 2 points (a safety fallback; callers should check empty()).
|
||||
double sample_gradient_curve(const GradientCurve& c, double t);
|
||||
|
||||
// Compute Fritsch-Carlson PCHIP default tangents for a sorted-by-x anchor list.
|
||||
// Result size == pts.size(). Useful for callers that need to know what tangent the
|
||||
// sampler would synthesize when m_in / m_out are NaN (e.g. the GUI's segment-bend
|
||||
// interaction that inserts a virtual anchor and reads back the surrounding tangents).
|
||||
std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts);
|
||||
|
||||
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
|
||||
unsigned char r2, unsigned char g2, unsigned char b2,
|
||||
float t,
|
||||
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b);
|
||||
|
||||
void filament_mixer_lerp_float(float r1, float g1, float b1,
|
||||
float r2, float g2, float b2,
|
||||
float t,
|
||||
float* out_r, float* out_g, float* out_b);
|
||||
|
||||
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
|
||||
float r2, float g2, float b2,
|
||||
float t,
|
||||
float* out_r, float* out_g, float* out_b);
|
||||
|
||||
// Blend two hex colors ("#RRGGBB") by ratio (0.0 ~ 1.0 for color_b).
|
||||
// Returns "#RRGGBB" string.
|
||||
std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b);
|
||||
|
||||
// Blend N hex colors by integer weights using polynomial pigment mixing.
|
||||
// Pairwise accumulation via filament_mixer_lerp. Returns "#RRGGBB".
|
||||
std::string blend_color_multi(const std::vector<std::string> &hex_colors,
|
||||
const std::vector<int> &weights);
|
||||
|
||||
// Parse comma-separated 1-based component IDs, e.g. "1,3" → {1, 3}.
|
||||
std::vector<unsigned int> parse_mixed_components(const std::string &str);
|
||||
|
||||
// Parse comma-separated ratio values, e.g. "0.7,0.3" → {0.7, 0.3}.
|
||||
// Returns equal ratios (1/n each) when str is empty or invalid.
|
||||
// Normalizes so the sum equals 1.0.
|
||||
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
|
||||
|
||||
// Returns true if any element in is_mixed is true.
|
||||
// ConfigOptionBools stores values as std::vector<unsigned char>.
|
||||
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
|
||||
|
||||
// Check which mixed filament slots have broken component references.
|
||||
// Returns 0-based indices of mixed slots whose components reference
|
||||
// filaments beyond num_physical (i.e., deleted filaments).
|
||||
std::vector<size_t> check_mixed_filament_integrity(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs,
|
||||
size_t num_physical);
|
||||
|
||||
// Expand mixed filament slots in an extruder list to their physical components.
|
||||
// Input/output are 0-based indices. Non-mixed slots pass through unchanged.
|
||||
// Result is sorted and deduplicated.
|
||||
std::vector<unsigned int> expand_mixed_filaments(
|
||||
const std::vector<unsigned int> &extruders_0based,
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs);
|
||||
|
||||
// Remap mixed filament component references after a physical filament is deleted.
|
||||
// del_1based: the 1-based index of the deleted physical filament.
|
||||
// For each mixed slot:
|
||||
// - if component == del_1based -> replace with 0 (sentinel for deleted/unselected)
|
||||
// - if component > del_1based -> decrement by 1
|
||||
void remap_mixed_components_on_delete(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
std::vector<std::string> &comp_strs,
|
||||
unsigned int del_1based);
|
||||
|
||||
// Check which mixed filament slots have type-mismatched components.
|
||||
// filament_types: type strings for physical filaments (0-based, size == num_physical).
|
||||
// Component IDs in comp_strs are 1-based; the function converts to 0-based to look up types.
|
||||
// Returns 0-based config indices of mixed slots with mismatched component types.
|
||||
std::vector<size_t> check_mixed_filament_type_consistency(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs,
|
||||
const std::vector<std::string> &filament_types);
|
||||
|
||||
// Expand mixed-slot IDs in geometric unprintable sets to their physical component IDs.
|
||||
// Each set entry that corresponds to a mixed slot is replaced by the slot's component
|
||||
// IDs (0-based). Non-mixed entries pass through unchanged.
|
||||
void expand_mixed_slots_in_unprintables(
|
||||
std::vector<std::set<int>> &unprintables,
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs);
|
||||
|
||||
// Clear any non-empty gradient-curve slot that parses to fewer than 2 control points.
|
||||
// Heals per-slot arrays corrupted by the legacy "|" separator collision between
|
||||
// PresetBundle::export_selections / load_selections (which used "|" as the inter-slot
|
||||
// delimiter) and serialize_gradient_curve / parse_gradient_curve (which use "|" as the
|
||||
// intra-slot control-point delimiter). Such a round-trip splits a multi-point curve
|
||||
// across adjacent slots, leaving single-point entries that fail MakerWorld's strict
|
||||
// "curve needs >= 2 points" check. Clearing them falls back to the linear range.
|
||||
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals);
|
||||
|
||||
// Validate mixed-color (混色) parameters. Returns error messages keyed by option name.
|
||||
// Slot details are included in the message text (1-based slot index).
|
||||
std::map<std::string, std::string> validate_mixed_filament_params(
|
||||
const std::vector<unsigned char> &is_mixed,
|
||||
const std::vector<std::string> &comp_strs,
|
||||
const std::vector<std::string> &ratio_strs,
|
||||
const std::vector<unsigned char> &gradient_flags,
|
||||
const std::vector<std::string> &gradient_range_strs,
|
||||
const std::vector<std::string> &gradient_curve_strs);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // SLIC3R_FILAMENT_MIXER_HPP
|
||||
@@ -0,0 +1,819 @@
|
||||
/*
|
||||
* FilamentMixer — Header-only C++ pigment color mixer
|
||||
*
|
||||
* Filament mixer implementation using a degree-4 polynomial regression
|
||||
* trained to approximate Mixbox behavior (Mean Delta-E ~2.07).
|
||||
* This library does not include Mixbox source code, binaries, or data files.
|
||||
*
|
||||
* Usage:
|
||||
* #include "FilamentMixerModel.hpp"
|
||||
*
|
||||
* unsigned char r, g, b;
|
||||
* filament_mixer::lerp(0, 33, 133, 252, 211, 0, 0.5f, &r, &g, &b);
|
||||
* // r=47, g=141, b=56 (blue + yellow → green)
|
||||
*
|
||||
* No dependencies beyond the C++ standard library.
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 Justin Hayes
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef FILAMENT_MIXER_MODEL_HPP
|
||||
#define FILAMENT_MIXER_MODEL_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace filament_mixer {
|
||||
namespace detail {
|
||||
|
||||
// BEGIN AUTO-GENERATED COEFFICIENTS
|
||||
// Auto-generated by scripts/export_poly_coefficients.py
|
||||
// Do not edit manually.
|
||||
// Degree-4 polynomial, 330 features, 7 inputs
|
||||
|
||||
static const int POLY_DEGREE = 4;
|
||||
static const int N_FEATURES = 330;
|
||||
static const int N_INPUTS = 7;
|
||||
|
||||
static const int POWERS[330][7] = {
|
||||
{0, 0, 0, 0, 0, 0, 0},
|
||||
{1, 0, 0, 0, 0, 0, 0},
|
||||
{0, 1, 0, 0, 0, 0, 0},
|
||||
{0, 0, 1, 0, 0, 0, 0},
|
||||
{0, 0, 0, 1, 0, 0, 0},
|
||||
{0, 0, 0, 0, 1, 0, 0},
|
||||
{0, 0, 0, 0, 0, 1, 0},
|
||||
{0, 0, 0, 0, 0, 0, 1},
|
||||
{2, 0, 0, 0, 0, 0, 0},
|
||||
{1, 1, 0, 0, 0, 0, 0},
|
||||
{1, 0, 1, 0, 0, 0, 0},
|
||||
{1, 0, 0, 1, 0, 0, 0},
|
||||
{1, 0, 0, 0, 1, 0, 0},
|
||||
{1, 0, 0, 0, 0, 1, 0},
|
||||
{1, 0, 0, 0, 0, 0, 1},
|
||||
{0, 2, 0, 0, 0, 0, 0},
|
||||
{0, 1, 1, 0, 0, 0, 0},
|
||||
{0, 1, 0, 1, 0, 0, 0},
|
||||
{0, 1, 0, 0, 1, 0, 0},
|
||||
{0, 1, 0, 0, 0, 1, 0},
|
||||
{0, 1, 0, 0, 0, 0, 1},
|
||||
{0, 0, 2, 0, 0, 0, 0},
|
||||
{0, 0, 1, 1, 0, 0, 0},
|
||||
{0, 0, 1, 0, 1, 0, 0},
|
||||
{0, 0, 1, 0, 0, 1, 0},
|
||||
{0, 0, 1, 0, 0, 0, 1},
|
||||
{0, 0, 0, 2, 0, 0, 0},
|
||||
{0, 0, 0, 1, 1, 0, 0},
|
||||
{0, 0, 0, 1, 0, 1, 0},
|
||||
{0, 0, 0, 1, 0, 0, 1},
|
||||
{0, 0, 0, 0, 2, 0, 0},
|
||||
{0, 0, 0, 0, 1, 1, 0},
|
||||
{0, 0, 0, 0, 1, 0, 1},
|
||||
{0, 0, 0, 0, 0, 2, 0},
|
||||
{0, 0, 0, 0, 0, 1, 1},
|
||||
{0, 0, 0, 0, 0, 0, 2},
|
||||
{3, 0, 0, 0, 0, 0, 0},
|
||||
{2, 1, 0, 0, 0, 0, 0},
|
||||
{2, 0, 1, 0, 0, 0, 0},
|
||||
{2, 0, 0, 1, 0, 0, 0},
|
||||
{2, 0, 0, 0, 1, 0, 0},
|
||||
{2, 0, 0, 0, 0, 1, 0},
|
||||
{2, 0, 0, 0, 0, 0, 1},
|
||||
{1, 2, 0, 0, 0, 0, 0},
|
||||
{1, 1, 1, 0, 0, 0, 0},
|
||||
{1, 1, 0, 1, 0, 0, 0},
|
||||
{1, 1, 0, 0, 1, 0, 0},
|
||||
{1, 1, 0, 0, 0, 1, 0},
|
||||
{1, 1, 0, 0, 0, 0, 1},
|
||||
{1, 0, 2, 0, 0, 0, 0},
|
||||
{1, 0, 1, 1, 0, 0, 0},
|
||||
{1, 0, 1, 0, 1, 0, 0},
|
||||
{1, 0, 1, 0, 0, 1, 0},
|
||||
{1, 0, 1, 0, 0, 0, 1},
|
||||
{1, 0, 0, 2, 0, 0, 0},
|
||||
{1, 0, 0, 1, 1, 0, 0},
|
||||
{1, 0, 0, 1, 0, 1, 0},
|
||||
{1, 0, 0, 1, 0, 0, 1},
|
||||
{1, 0, 0, 0, 2, 0, 0},
|
||||
{1, 0, 0, 0, 1, 1, 0},
|
||||
{1, 0, 0, 0, 1, 0, 1},
|
||||
{1, 0, 0, 0, 0, 2, 0},
|
||||
{1, 0, 0, 0, 0, 1, 1},
|
||||
{1, 0, 0, 0, 0, 0, 2},
|
||||
{0, 3, 0, 0, 0, 0, 0},
|
||||
{0, 2, 1, 0, 0, 0, 0},
|
||||
{0, 2, 0, 1, 0, 0, 0},
|
||||
{0, 2, 0, 0, 1, 0, 0},
|
||||
{0, 2, 0, 0, 0, 1, 0},
|
||||
{0, 2, 0, 0, 0, 0, 1},
|
||||
{0, 1, 2, 0, 0, 0, 0},
|
||||
{0, 1, 1, 1, 0, 0, 0},
|
||||
{0, 1, 1, 0, 1, 0, 0},
|
||||
{0, 1, 1, 0, 0, 1, 0},
|
||||
{0, 1, 1, 0, 0, 0, 1},
|
||||
{0, 1, 0, 2, 0, 0, 0},
|
||||
{0, 1, 0, 1, 1, 0, 0},
|
||||
{0, 1, 0, 1, 0, 1, 0},
|
||||
{0, 1, 0, 1, 0, 0, 1},
|
||||
{0, 1, 0, 0, 2, 0, 0},
|
||||
{0, 1, 0, 0, 1, 1, 0},
|
||||
{0, 1, 0, 0, 1, 0, 1},
|
||||
{0, 1, 0, 0, 0, 2, 0},
|
||||
{0, 1, 0, 0, 0, 1, 1},
|
||||
{0, 1, 0, 0, 0, 0, 2},
|
||||
{0, 0, 3, 0, 0, 0, 0},
|
||||
{0, 0, 2, 1, 0, 0, 0},
|
||||
{0, 0, 2, 0, 1, 0, 0},
|
||||
{0, 0, 2, 0, 0, 1, 0},
|
||||
{0, 0, 2, 0, 0, 0, 1},
|
||||
{0, 0, 1, 2, 0, 0, 0},
|
||||
{0, 0, 1, 1, 1, 0, 0},
|
||||
{0, 0, 1, 1, 0, 1, 0},
|
||||
{0, 0, 1, 1, 0, 0, 1},
|
||||
{0, 0, 1, 0, 2, 0, 0},
|
||||
{0, 0, 1, 0, 1, 1, 0},
|
||||
{0, 0, 1, 0, 1, 0, 1},
|
||||
{0, 0, 1, 0, 0, 2, 0},
|
||||
{0, 0, 1, 0, 0, 1, 1},
|
||||
{0, 0, 1, 0, 0, 0, 2},
|
||||
{0, 0, 0, 3, 0, 0, 0},
|
||||
{0, 0, 0, 2, 1, 0, 0},
|
||||
{0, 0, 0, 2, 0, 1, 0},
|
||||
{0, 0, 0, 2, 0, 0, 1},
|
||||
{0, 0, 0, 1, 2, 0, 0},
|
||||
{0, 0, 0, 1, 1, 1, 0},
|
||||
{0, 0, 0, 1, 1, 0, 1},
|
||||
{0, 0, 0, 1, 0, 2, 0},
|
||||
{0, 0, 0, 1, 0, 1, 1},
|
||||
{0, 0, 0, 1, 0, 0, 2},
|
||||
{0, 0, 0, 0, 3, 0, 0},
|
||||
{0, 0, 0, 0, 2, 1, 0},
|
||||
{0, 0, 0, 0, 2, 0, 1},
|
||||
{0, 0, 0, 0, 1, 2, 0},
|
||||
{0, 0, 0, 0, 1, 1, 1},
|
||||
{0, 0, 0, 0, 1, 0, 2},
|
||||
{0, 0, 0, 0, 0, 3, 0},
|
||||
{0, 0, 0, 0, 0, 2, 1},
|
||||
{0, 0, 0, 0, 0, 1, 2},
|
||||
{0, 0, 0, 0, 0, 0, 3},
|
||||
{4, 0, 0, 0, 0, 0, 0},
|
||||
{3, 1, 0, 0, 0, 0, 0},
|
||||
{3, 0, 1, 0, 0, 0, 0},
|
||||
{3, 0, 0, 1, 0, 0, 0},
|
||||
{3, 0, 0, 0, 1, 0, 0},
|
||||
{3, 0, 0, 0, 0, 1, 0},
|
||||
{3, 0, 0, 0, 0, 0, 1},
|
||||
{2, 2, 0, 0, 0, 0, 0},
|
||||
{2, 1, 1, 0, 0, 0, 0},
|
||||
{2, 1, 0, 1, 0, 0, 0},
|
||||
{2, 1, 0, 0, 1, 0, 0},
|
||||
{2, 1, 0, 0, 0, 1, 0},
|
||||
{2, 1, 0, 0, 0, 0, 1},
|
||||
{2, 0, 2, 0, 0, 0, 0},
|
||||
{2, 0, 1, 1, 0, 0, 0},
|
||||
{2, 0, 1, 0, 1, 0, 0},
|
||||
{2, 0, 1, 0, 0, 1, 0},
|
||||
{2, 0, 1, 0, 0, 0, 1},
|
||||
{2, 0, 0, 2, 0, 0, 0},
|
||||
{2, 0, 0, 1, 1, 0, 0},
|
||||
{2, 0, 0, 1, 0, 1, 0},
|
||||
{2, 0, 0, 1, 0, 0, 1},
|
||||
{2, 0, 0, 0, 2, 0, 0},
|
||||
{2, 0, 0, 0, 1, 1, 0},
|
||||
{2, 0, 0, 0, 1, 0, 1},
|
||||
{2, 0, 0, 0, 0, 2, 0},
|
||||
{2, 0, 0, 0, 0, 1, 1},
|
||||
{2, 0, 0, 0, 0, 0, 2},
|
||||
{1, 3, 0, 0, 0, 0, 0},
|
||||
{1, 2, 1, 0, 0, 0, 0},
|
||||
{1, 2, 0, 1, 0, 0, 0},
|
||||
{1, 2, 0, 0, 1, 0, 0},
|
||||
{1, 2, 0, 0, 0, 1, 0},
|
||||
{1, 2, 0, 0, 0, 0, 1},
|
||||
{1, 1, 2, 0, 0, 0, 0},
|
||||
{1, 1, 1, 1, 0, 0, 0},
|
||||
{1, 1, 1, 0, 1, 0, 0},
|
||||
{1, 1, 1, 0, 0, 1, 0},
|
||||
{1, 1, 1, 0, 0, 0, 1},
|
||||
{1, 1, 0, 2, 0, 0, 0},
|
||||
{1, 1, 0, 1, 1, 0, 0},
|
||||
{1, 1, 0, 1, 0, 1, 0},
|
||||
{1, 1, 0, 1, 0, 0, 1},
|
||||
{1, 1, 0, 0, 2, 0, 0},
|
||||
{1, 1, 0, 0, 1, 1, 0},
|
||||
{1, 1, 0, 0, 1, 0, 1},
|
||||
{1, 1, 0, 0, 0, 2, 0},
|
||||
{1, 1, 0, 0, 0, 1, 1},
|
||||
{1, 1, 0, 0, 0, 0, 2},
|
||||
{1, 0, 3, 0, 0, 0, 0},
|
||||
{1, 0, 2, 1, 0, 0, 0},
|
||||
{1, 0, 2, 0, 1, 0, 0},
|
||||
{1, 0, 2, 0, 0, 1, 0},
|
||||
{1, 0, 2, 0, 0, 0, 1},
|
||||
{1, 0, 1, 2, 0, 0, 0},
|
||||
{1, 0, 1, 1, 1, 0, 0},
|
||||
{1, 0, 1, 1, 0, 1, 0},
|
||||
{1, 0, 1, 1, 0, 0, 1},
|
||||
{1, 0, 1, 0, 2, 0, 0},
|
||||
{1, 0, 1, 0, 1, 1, 0},
|
||||
{1, 0, 1, 0, 1, 0, 1},
|
||||
{1, 0, 1, 0, 0, 2, 0},
|
||||
{1, 0, 1, 0, 0, 1, 1},
|
||||
{1, 0, 1, 0, 0, 0, 2},
|
||||
{1, 0, 0, 3, 0, 0, 0},
|
||||
{1, 0, 0, 2, 1, 0, 0},
|
||||
{1, 0, 0, 2, 0, 1, 0},
|
||||
{1, 0, 0, 2, 0, 0, 1},
|
||||
{1, 0, 0, 1, 2, 0, 0},
|
||||
{1, 0, 0, 1, 1, 1, 0},
|
||||
{1, 0, 0, 1, 1, 0, 1},
|
||||
{1, 0, 0, 1, 0, 2, 0},
|
||||
{1, 0, 0, 1, 0, 1, 1},
|
||||
{1, 0, 0, 1, 0, 0, 2},
|
||||
{1, 0, 0, 0, 3, 0, 0},
|
||||
{1, 0, 0, 0, 2, 1, 0},
|
||||
{1, 0, 0, 0, 2, 0, 1},
|
||||
{1, 0, 0, 0, 1, 2, 0},
|
||||
{1, 0, 0, 0, 1, 1, 1},
|
||||
{1, 0, 0, 0, 1, 0, 2},
|
||||
{1, 0, 0, 0, 0, 3, 0},
|
||||
{1, 0, 0, 0, 0, 2, 1},
|
||||
{1, 0, 0, 0, 0, 1, 2},
|
||||
{1, 0, 0, 0, 0, 0, 3},
|
||||
{0, 4, 0, 0, 0, 0, 0},
|
||||
{0, 3, 1, 0, 0, 0, 0},
|
||||
{0, 3, 0, 1, 0, 0, 0},
|
||||
{0, 3, 0, 0, 1, 0, 0},
|
||||
{0, 3, 0, 0, 0, 1, 0},
|
||||
{0, 3, 0, 0, 0, 0, 1},
|
||||
{0, 2, 2, 0, 0, 0, 0},
|
||||
{0, 2, 1, 1, 0, 0, 0},
|
||||
{0, 2, 1, 0, 1, 0, 0},
|
||||
{0, 2, 1, 0, 0, 1, 0},
|
||||
{0, 2, 1, 0, 0, 0, 1},
|
||||
{0, 2, 0, 2, 0, 0, 0},
|
||||
{0, 2, 0, 1, 1, 0, 0},
|
||||
{0, 2, 0, 1, 0, 1, 0},
|
||||
{0, 2, 0, 1, 0, 0, 1},
|
||||
{0, 2, 0, 0, 2, 0, 0},
|
||||
{0, 2, 0, 0, 1, 1, 0},
|
||||
{0, 2, 0, 0, 1, 0, 1},
|
||||
{0, 2, 0, 0, 0, 2, 0},
|
||||
{0, 2, 0, 0, 0, 1, 1},
|
||||
{0, 2, 0, 0, 0, 0, 2},
|
||||
{0, 1, 3, 0, 0, 0, 0},
|
||||
{0, 1, 2, 1, 0, 0, 0},
|
||||
{0, 1, 2, 0, 1, 0, 0},
|
||||
{0, 1, 2, 0, 0, 1, 0},
|
||||
{0, 1, 2, 0, 0, 0, 1},
|
||||
{0, 1, 1, 2, 0, 0, 0},
|
||||
{0, 1, 1, 1, 1, 0, 0},
|
||||
{0, 1, 1, 1, 0, 1, 0},
|
||||
{0, 1, 1, 1, 0, 0, 1},
|
||||
{0, 1, 1, 0, 2, 0, 0},
|
||||
{0, 1, 1, 0, 1, 1, 0},
|
||||
{0, 1, 1, 0, 1, 0, 1},
|
||||
{0, 1, 1, 0, 0, 2, 0},
|
||||
{0, 1, 1, 0, 0, 1, 1},
|
||||
{0, 1, 1, 0, 0, 0, 2},
|
||||
{0, 1, 0, 3, 0, 0, 0},
|
||||
{0, 1, 0, 2, 1, 0, 0},
|
||||
{0, 1, 0, 2, 0, 1, 0},
|
||||
{0, 1, 0, 2, 0, 0, 1},
|
||||
{0, 1, 0, 1, 2, 0, 0},
|
||||
{0, 1, 0, 1, 1, 1, 0},
|
||||
{0, 1, 0, 1, 1, 0, 1},
|
||||
{0, 1, 0, 1, 0, 2, 0},
|
||||
{0, 1, 0, 1, 0, 1, 1},
|
||||
{0, 1, 0, 1, 0, 0, 2},
|
||||
{0, 1, 0, 0, 3, 0, 0},
|
||||
{0, 1, 0, 0, 2, 1, 0},
|
||||
{0, 1, 0, 0, 2, 0, 1},
|
||||
{0, 1, 0, 0, 1, 2, 0},
|
||||
{0, 1, 0, 0, 1, 1, 1},
|
||||
{0, 1, 0, 0, 1, 0, 2},
|
||||
{0, 1, 0, 0, 0, 3, 0},
|
||||
{0, 1, 0, 0, 0, 2, 1},
|
||||
{0, 1, 0, 0, 0, 1, 2},
|
||||
{0, 1, 0, 0, 0, 0, 3},
|
||||
{0, 0, 4, 0, 0, 0, 0},
|
||||
{0, 0, 3, 1, 0, 0, 0},
|
||||
{0, 0, 3, 0, 1, 0, 0},
|
||||
{0, 0, 3, 0, 0, 1, 0},
|
||||
{0, 0, 3, 0, 0, 0, 1},
|
||||
{0, 0, 2, 2, 0, 0, 0},
|
||||
{0, 0, 2, 1, 1, 0, 0},
|
||||
{0, 0, 2, 1, 0, 1, 0},
|
||||
{0, 0, 2, 1, 0, 0, 1},
|
||||
{0, 0, 2, 0, 2, 0, 0},
|
||||
{0, 0, 2, 0, 1, 1, 0},
|
||||
{0, 0, 2, 0, 1, 0, 1},
|
||||
{0, 0, 2, 0, 0, 2, 0},
|
||||
{0, 0, 2, 0, 0, 1, 1},
|
||||
{0, 0, 2, 0, 0, 0, 2},
|
||||
{0, 0, 1, 3, 0, 0, 0},
|
||||
{0, 0, 1, 2, 1, 0, 0},
|
||||
{0, 0, 1, 2, 0, 1, 0},
|
||||
{0, 0, 1, 2, 0, 0, 1},
|
||||
{0, 0, 1, 1, 2, 0, 0},
|
||||
{0, 0, 1, 1, 1, 1, 0},
|
||||
{0, 0, 1, 1, 1, 0, 1},
|
||||
{0, 0, 1, 1, 0, 2, 0},
|
||||
{0, 0, 1, 1, 0, 1, 1},
|
||||
{0, 0, 1, 1, 0, 0, 2},
|
||||
{0, 0, 1, 0, 3, 0, 0},
|
||||
{0, 0, 1, 0, 2, 1, 0},
|
||||
{0, 0, 1, 0, 2, 0, 1},
|
||||
{0, 0, 1, 0, 1, 2, 0},
|
||||
{0, 0, 1, 0, 1, 1, 1},
|
||||
{0, 0, 1, 0, 1, 0, 2},
|
||||
{0, 0, 1, 0, 0, 3, 0},
|
||||
{0, 0, 1, 0, 0, 2, 1},
|
||||
{0, 0, 1, 0, 0, 1, 2},
|
||||
{0, 0, 1, 0, 0, 0, 3},
|
||||
{0, 0, 0, 4, 0, 0, 0},
|
||||
{0, 0, 0, 3, 1, 0, 0},
|
||||
{0, 0, 0, 3, 0, 1, 0},
|
||||
{0, 0, 0, 3, 0, 0, 1},
|
||||
{0, 0, 0, 2, 2, 0, 0},
|
||||
{0, 0, 0, 2, 1, 1, 0},
|
||||
{0, 0, 0, 2, 1, 0, 1},
|
||||
{0, 0, 0, 2, 0, 2, 0},
|
||||
{0, 0, 0, 2, 0, 1, 1},
|
||||
{0, 0, 0, 2, 0, 0, 2},
|
||||
{0, 0, 0, 1, 3, 0, 0},
|
||||
{0, 0, 0, 1, 2, 1, 0},
|
||||
{0, 0, 0, 1, 2, 0, 1},
|
||||
{0, 0, 0, 1, 1, 2, 0},
|
||||
{0, 0, 0, 1, 1, 1, 1},
|
||||
{0, 0, 0, 1, 1, 0, 2},
|
||||
{0, 0, 0, 1, 0, 3, 0},
|
||||
{0, 0, 0, 1, 0, 2, 1},
|
||||
{0, 0, 0, 1, 0, 1, 2},
|
||||
{0, 0, 0, 1, 0, 0, 3},
|
||||
{0, 0, 0, 0, 4, 0, 0},
|
||||
{0, 0, 0, 0, 3, 1, 0},
|
||||
{0, 0, 0, 0, 3, 0, 1},
|
||||
{0, 0, 0, 0, 2, 2, 0},
|
||||
{0, 0, 0, 0, 2, 1, 1},
|
||||
{0, 0, 0, 0, 2, 0, 2},
|
||||
{0, 0, 0, 0, 1, 3, 0},
|
||||
{0, 0, 0, 0, 1, 2, 1},
|
||||
{0, 0, 0, 0, 1, 1, 2},
|
||||
{0, 0, 0, 0, 1, 0, 3},
|
||||
{0, 0, 0, 0, 0, 4, 0},
|
||||
{0, 0, 0, 0, 0, 3, 1},
|
||||
{0, 0, 0, 0, 0, 2, 2},
|
||||
{0, 0, 0, 0, 0, 1, 3},
|
||||
{0, 0, 0, 0, 0, 0, 4}
|
||||
};
|
||||
|
||||
static const double COEF[330][3] = {
|
||||
{8.70954844857314666e-12, 1.27926950848359881e-09, -2.06865474316332923e-09},
|
||||
{1.05783308354771544e+00, -8.02119209663359686e-03, -7.88705651445470723e-02},
|
||||
{1.35905954452774837e-02, 8.71267975138422468e-01, 1.04898760410704936e-01},
|
||||
{-4.16452026099768252e-02, 1.75465381596434100e-02, 1.00224594702931546e+00},
|
||||
{4.50321316661211821e-02, -7.11409155427628892e-02, 3.91232300778902690e-03},
|
||||
{1.76675507851922452e-02, -1.32709276116036640e-01, 6.36935270589509828e-02},
|
||||
{-5.23434830565911030e-02, 3.77681739012521722e-02, -2.08691145087504179e-02},
|
||||
{-2.33722556520224792e-03, -1.57542611462692145e-03, -3.05158628452478807e-03},
|
||||
{-8.87678609044812990e-04, 3.83194388837734693e-04, 1.37779212442523083e-03},
|
||||
{-2.11519042076831979e-03, 5.82337362515735358e-04, 2.24055108941204821e-04},
|
||||
{4.61545125563611917e-04, 7.72869451707915893e-04, -1.10800630143346882e-03},
|
||||
{1.05937484157345879e-03, -3.14448681732842211e-04, -1.75129182446198098e-03},
|
||||
{1.49045689016363055e-03, -2.09220860101674106e-04, 5.93100338908187697e-04},
|
||||
{-3.51246656293852696e-04, -8.20743017485394289e-04, 5.71854064480802862e-04},
|
||||
{-9.18204643629581319e-01, -2.27788122702773155e-01, 6.39980793022790623e-02},
|
||||
{9.24243491377523679e-05, 7.32841332381495400e-04, -1.55219718415109450e-03},
|
||||
{7.13695056804217989e-04, -8.46467621879685712e-05, 6.50202947442505750e-04},
|
||||
{1.66640864747485983e-03, -1.24492362771216523e-04, 2.68236502346156410e-04},
|
||||
{-7.20253644860527516e-04, 7.81434220384157334e-04, 1.12661089007361367e-03},
|
||||
{-6.83033334365238206e-05, 7.27742627159490762e-04, -1.78048843835204584e-03},
|
||||
{-3.13431571993316588e-02, -8.57604034845650287e-01, -2.57225920656276863e-01},
|
||||
{-6.47867200595898341e-05, -1.16688982572457655e-03, 1.14174511750260031e-03},
|
||||
{-5.00713925613324338e-04, -6.87598082111323477e-04, 6.20598069880440176e-04},
|
||||
{-8.56716727659588957e-05, 9.74478786593559361e-04, -1.65892838405139512e-03},
|
||||
{6.53468478750158263e-04, 7.51662000672516676e-04, -6.73196326298856570e-04},
|
||||
{-4.42539011000103941e-02, -2.01965359697350230e-02, -9.94663493761314355e-01},
|
||||
{-7.39107395392403087e-04, 5.28870828612476996e-04, 1.00947183860234540e-03},
|
||||
{-2.06577300933763214e-03, 9.60215813758718011e-04, -3.27993888180819421e-04},
|
||||
{3.47783280638377555e-04, 8.41824316850705743e-04, -8.87458944147930993e-04},
|
||||
{1.20960551709587905e+00, -7.07660818059813873e-02, -8.56332806008946491e-03},
|
||||
{2.11116509318935269e-04, 7.68490846994171776e-04, -1.63228995491542417e-03},
|
||||
{6.47698075356516103e-04, -4.20589129268072884e-04, 1.18354001300614896e-03},
|
||||
{-2.78795945253848716e-02, 1.22199201000304547e+00, -2.07383075858847743e-01},
|
||||
{-5.32457386680677347e-05, -9.58027320315790677e-04, 9.89667309649038679e-04},
|
||||
{-9.03932426306289782e-02, -4.00969232187064692e-02, 1.26285611182120072e+00},
|
||||
{-2.19453630740322871e-03, -1.21893190049422620e-03, -1.92293368093085417e-03},
|
||||
{1.72950845415964505e-06, -8.93952511560151819e-09, -6.14874900641340649e-06},
|
||||
{8.02644554976326974e-06, -6.42543741723487294e-06, -6.07103419227907060e-06},
|
||||
{3.20307552755319525e-06, -4.83533743093466500e-06, 9.13563764113473065e-07},
|
||||
{-2.18105804067510178e-06, 6.19595552598436322e-07, 5.21392855381760945e-06},
|
||||
{-2.43310123604345563e-06, 2.17201813434465818e-06, 1.94098874242362718e-07},
|
||||
{-1.56293672065252465e-06, 3.95256011818110372e-06, 1.68792962079201969e-06},
|
||||
{-1.37567295252127852e-03, 3.59746071987262106e-04, 7.38927139000157259e-05},
|
||||
{4.27822004137219658e-06, -8.80187479967658548e-07, 2.29453131891411977e-06},
|
||||
{7.68758937964332534e-06, 2.40909410585557829e-07, 4.69351234070854509e-06},
|
||||
{-2.87166709944317033e-06, 7.60223902901142716e-07, 4.57864913314467992e-06},
|
||||
{-4.01295140267654560e-06, 2.65929275888376483e-06, -2.36575067819565221e-06},
|
||||
{2.32693030513910805e-07, 2.28814396769890308e-06, 1.83526107699893970e-07},
|
||||
{-2.18213927011287265e-03, 1.65013083920367864e-03, 2.31992998847323087e-04},
|
||||
{-7.70829764693697905e-06, 4.23888841240673345e-07, 7.30018322002944087e-06},
|
||||
{-1.23111329452911533e-06, 1.50076529718910084e-06, -1.91139744928209288e-06},
|
||||
{-1.68872756433485760e-06, 1.03254236824697979e-06, -1.72081108163607555e-06},
|
||||
{1.64276928199709460e-06, -4.96350219553231067e-07, -1.46349385185670297e-06},
|
||||
{1.12731767057843682e-03, 5.03104281148445223e-04, 1.36398977654308994e-03},
|
||||
{-1.05449609518089293e-06, -4.06952115309007489e-07, 3.53062441379482783e-06},
|
||||
{-1.98745923822574166e-06, 4.98021943693208180e-07, 3.92645061370218429e-06},
|
||||
{-1.55569377977005097e-07, -4.00262856484093037e-07, -2.49609122397048688e-06},
|
||||
{2.18005022830924673e-03, -4.10275057064835439e-05, -2.59776311836759947e-04},
|
||||
{5.41337439827552225e-07, -1.88603932528607146e-06, -2.06428606152470051e-06},
|
||||
{-6.03243799807140491e-06, -3.75067864464502022e-06, -3.05702776851046742e-06},
|
||||
{2.30038011634901016e-03, -1.32581161861259635e-03, -1.07680096899188406e-03},
|
||||
{4.46773877910556887e-06, 1.85008408528524772e-08, -2.72851357570281713e-06},
|
||||
{-1.49177636513049289e-03, -1.91426739654176659e-04, -1.71206384332753194e-03},
|
||||
{2.31661325589237743e-02, 2.26540538563063554e-01, 5.42330337046266139e-02},
|
||||
{-1.40563059963100256e-06, -4.50551806294901061e-06, 8.87542894832671347e-06},
|
||||
{-1.66780916452391459e-06, 4.12065434881171526e-06, -3.55865035776836702e-06},
|
||||
{2.71536622051954390e-07, -3.08564858926584692e-06, -1.52164363662402047e-06},
|
||||
{2.66659632027280158e-06, -1.19436686895073481e-06, -3.25738306279285683e-06},
|
||||
{-1.43666282346327501e-06, -2.51923473623639690e-06, 5.21205120344175876e-06},
|
||||
{2.82954522469612199e-04, -1.59147454710008968e-03, 1.27685773978167098e-03},
|
||||
{-3.99471240294241303e-06, 9.97323772325767188e-08, -5.28196823261495307e-06},
|
||||
{-6.39858432699424995e-06, -4.59897864440506933e-06, -2.39736149785715891e-06},
|
||||
{2.89457420106498109e-06, -3.10427512149489757e-06, 9.75553221437691631e-07},
|
||||
{-8.96518259720091581e-07, -5.53996694461914366e-06, 1.03733964032237669e-05},
|
||||
{8.82130497168875905e-04, -2.33618402105562365e-03, 1.35100410641244379e-03},
|
||||
{-2.14088521029685841e-06, 2.59005410360388117e-06, -9.78713171504927426e-08},
|
||||
{-4.50668337071552516e-06, 3.58808570076458002e-06, -1.56159349007541082e-06},
|
||||
{-1.52345101244247272e-06, 2.21066768791959578e-06, -2.19555898547246775e-06},
|
||||
{2.07334042074768356e-03, -1.56333498489329517e-03, -5.53762940364141767e-04},
|
||||
{2.22151748134440108e-06, -4.74729938900429749e-07, -3.46744150304684889e-06},
|
||||
{2.95389009221172505e-06, -2.96312023445686329e-06, -9.00385068308695580e-07},
|
||||
{-6.47780848348620771e-04, 2.38772263398574292e-03, -8.93908589731968019e-04},
|
||||
{9.69501567645025819e-07, 2.41432205872957328e-06, 5.56908291093893837e-07},
|
||||
{-6.33392066185247586e-04, 2.38613844267241120e-03, -1.05383725637261472e-03},
|
||||
{6.76250135616376785e-02, -5.57799579151454852e-02, 1.83393652374666566e-01},
|
||||
{3.53986894266120067e-06, 5.92996717102502093e-06, -7.32378536156402804e-06},
|
||||
{5.69667193362453916e-06, 1.20219201908705218e-06, -4.56663805956276925e-06},
|
||||
{7.11494218295222192e-07, 2.93069858359131137e-06, 1.23210839732268429e-07},
|
||||
{-3.41917893741799928e-06, -1.47435291776966751e-06, 1.07397354370819542e-06},
|
||||
{7.30931882734254710e-04, 1.15433149094644884e-03, -2.40026982569019722e-03},
|
||||
{-1.22780859907432871e-06, 2.29287908084027789e-06, 1.84270754640877832e-06},
|
||||
{7.71579140080615178e-07, 2.92378122615943208e-06, -1.91800935486416413e-07},
|
||||
{-3.76107279903559188e-07, -1.83159743461489867e-06, 8.17089655984204466e-07},
|
||||
{-1.10830882430058061e-03, -5.10908079549339251e-04, -1.77835176235151705e-03},
|
||||
{-1.26839781743699406e-06, -2.86942252006448415e-06, 4.47464983859263005e-06},
|
||||
{-1.44518716284694482e-06, -7.03360635528004451e-06, 1.04898109513258675e-05},
|
||||
{-4.98687888007460470e-04, 1.86990180752567262e-03, -1.24341018156770089e-03},
|
||||
{-2.90479801332704790e-06, -9.24272269110706229e-07, 7.56354222045119151e-07},
|
||||
{-1.16451534008294149e-03, -2.34216801827852273e-03, 4.91479264672447288e-03},
|
||||
{-7.70970926241258958e-02, 9.35855573900774423e-02, 1.50623807158846906e-01},
|
||||
{1.14039905307547484e-06, -1.80664235182388840e-07, -5.15527441317074897e-06},
|
||||
{7.50559587697416375e-06, -6.23982034686780714e-06, -5.01245198064126721e-06},
|
||||
{2.37840954889385892e-06, -4.15663063190341991e-06, 1.93118829429697603e-06},
|
||||
{-1.54903048110950777e-03, 2.65832194444263125e-04, 5.34401520444913940e-04},
|
||||
{4.00040634507183718e-06, -2.43965474694277443e-06, 2.88683251413283937e-06},
|
||||
{7.72301916160400559e-06, -9.54300275625495457e-07, 5.50777546561020959e-06},
|
||||
{-2.28103126593574368e-03, 1.02658341009706066e-03, 1.22010567464172614e-03},
|
||||
{-6.32818026002207601e-06, 9.83088209200334157e-07, 5.24316808343458507e-06},
|
||||
{1.37175660779395581e-03, 4.01188715721313943e-04, 7.59370199245276625e-04},
|
||||
{-3.33184694847917573e-01, 7.82846225823195241e-02, -9.94270054263078074e-02},
|
||||
{-1.70108770909324636e-06, -5.10749831734438279e-06, 9.80267482880020635e-06},
|
||||
{-1.79301365419055891e-06, 4.44839673308561508e-06, -3.83837422072638712e-06},
|
||||
{1.71911692904483371e-04, -1.56077480341044431e-03, 1.30725115579017584e-03},
|
||||
{-3.55763938679129477e-06, 1.20558966207589408e-06, -5.94340114624253291e-06},
|
||||
{1.02325453537648178e-03, -1.52640960762801372e-03, 3.10973117856692537e-04},
|
||||
{3.81842873295820109e-03, -3.02114884453467680e-01, 2.78264587142456665e-01},
|
||||
{3.46123498726202961e-06, 5.05929187103208375e-06, -6.85764673719752027e-06},
|
||||
{4.47228353489932293e-04, 9.60672217798415784e-04, -2.19382758010531077e-03},
|
||||
{2.22711833124298791e-01, -4.14141995162802465e-02, -4.27998216564745015e-01},
|
||||
{-1.78271151817048783e-03, -9.81039111371464307e-04, -1.37513011841553174e-03},
|
||||
{3.35305394560947434e-10, -1.26710751613412498e-09, 3.54248685940916630e-09},
|
||||
{-9.26917423371698135e-09, 6.21190912597491263e-09, 1.86942252233812667e-08},
|
||||
{-1.56687696151180944e-09, -5.44315731376698864e-09, 1.93822974337010123e-09},
|
||||
{7.52897716393974292e-10, -3.48923168136394679e-10, -5.94217786087369859e-10},
|
||||
{2.52116855170569920e-10, -2.48216903975251313e-09, 1.01699001303634518e-09},
|
||||
{3.72215577457146729e-09, 4.51910314724912610e-10, -6.15361639422218332e-09},
|
||||
{-2.62088816666700142e-07, 3.23631086683010168e-07, 8.85302852722882894e-07},
|
||||
{-1.30537319842360944e-08, 1.46808588619151692e-08, 2.67574040702101001e-09},
|
||||
{-1.23991327621864045e-08, 2.61298349069072344e-08, -4.58919307373337193e-09},
|
||||
{5.03079244928983371e-09, -6.73783119575777079e-10, -1.13935871848269699e-08},
|
||||
{9.09065785148488459e-09, -1.04304054004966673e-08, -3.23123813816827976e-09},
|
||||
{9.55627910137479830e-10, -1.41129563591135820e-08, -1.75594400131373618e-09},
|
||||
{-1.05549669436946769e-07, 8.47284096194811896e-08, 6.70761880091491625e-07},
|
||||
{-5.92079330008488114e-10, 6.31702118392141188e-09, -4.51534448719925763e-09},
|
||||
{-1.04033970327321867e-09, 4.67775485013532943e-09, 2.79348504744758586e-09},
|
||||
{5.38758108958869997e-09, -9.55380699552144108e-09, 6.16488249338686956e-11},
|
||||
{1.12057409185073453e-09, -3.00645183748393663e-09, -2.14940637510707688e-09},
|
||||
{-6.27004681934967278e-07, 8.59159786402940127e-07, 2.73192537668387470e-07},
|
||||
{7.36784189214745311e-10, -8.12761968838060511e-10, -2.43226564583531868e-09},
|
||||
{1.25546123497244366e-09, -6.98609614602219153e-10, -5.29894812750786315e-09},
|
||||
{-8.88351475714088679e-10, 1.37132565025677167e-09, 1.92497813869541012e-09},
|
||||
{6.10992637326349119e-07, -6.13496367368217277e-07, -2.19901889726877020e-06},
|
||||
{-8.59090437677068053e-11, 2.72772732179404898e-09, 1.54554039011323141e-09},
|
||||
{-4.58798915525804318e-10, 4.54384851966693759e-09, 3.63189350816028877e-09},
|
||||
{9.93115786933340683e-08, 1.63700862245048928e-07, -1.71397937400244449e-07},
|
||||
{-1.62985361318312982e-09, -3.10762126448649312e-09, 1.76193495557419588e-09},
|
||||
{6.27207737564569601e-07, -1.49343052365004934e-06, 8.16168870109573730e-08},
|
||||
{1.42518738380244172e-03, -3.47531891583186285e-04, -2.98661838800559913e-04},
|
||||
{8.98157254125564464e-09, -8.24242643235328920e-09, -5.34769730234363472e-09},
|
||||
{-2.17776999489327494e-08, -4.47141107473569832e-09, -1.10218517090920898e-08},
|
||||
{3.19614509858290319e-09, -3.32861183754973311e-09, 9.92016746526047655e-11},
|
||||
{-2.91660393059167689e-09, 5.59829099744391101e-09, 1.70080685646389895e-09},
|
||||
{1.22479524179014421e-09, 9.20737683318684219e-09, -1.10618757209746121e-10},
|
||||
{7.70594587548882257e-09, -1.33267446898667659e-06, 4.52812675308736368e-07},
|
||||
{9.46080642993951670e-09, -1.95483249032513129e-08, -1.23592694620255905e-08},
|
||||
{-2.02330094345448686e-09, 1.18198534293512125e-10, 2.34746776184291406e-09},
|
||||
{4.00839940406516604e-09, -4.80716730311137042e-09, 5.25802457129742606e-09},
|
||||
{-2.53115202408782380e-09, 2.05563177591017165e-10, 5.46003270374129102e-09},
|
||||
{3.24841319972028232e-08, -1.24284705839720552e-06, 4.97326549863015555e-07},
|
||||
{1.37729661009444726e-09, -1.67903983772088594e-09, -5.62083748989472554e-09},
|
||||
{-3.53256937590806785e-10, 4.49320892992322030e-09, -4.02300486673778934e-09},
|
||||
{2.48976475547557641e-09, -6.97256366533061112e-09, 1.43185084622299286e-09},
|
||||
{-4.38617299338556199e-09, 9.45081248826811111e-08, -2.91197460585562728e-07},
|
||||
{3.24429103026879773e-09, -1.71647943601749287e-09, 2.71076100455402980e-09},
|
||||
{3.86933235105302309e-09, -2.82628156988984358e-09, 8.24455756442965537e-09},
|
||||
{-7.46614068323353530e-07, 1.27696340529665289e-06, 6.88413034833322557e-07},
|
||||
{-5.78118683480788320e-09, 1.34319005917760137e-09, -1.15898873831454807e-09},
|
||||
{4.42686972671260670e-07, 6.41810588767341775e-07, -1.16058405342719939e-08},
|
||||
{2.24399192788231686e-03, -1.35129336477888174e-03, -7.39944244498236844e-04},
|
||||
{7.47869199901884940e-09, -2.68762612165573955e-09, -7.41584788022109365e-09},
|
||||
{1.80867308283150230e-09, -2.21500551234043996e-09, 1.86995768869380186e-09},
|
||||
{-5.05514829302056157e-09, 4.74048706539109688e-09, 2.52998993977016085e-09},
|
||||
{1.32441967115592973e-09, 5.70339246663831290e-09, 7.13448300437846683e-10},
|
||||
{1.19767475292940212e-06, 6.72445227582811568e-07, -1.97500319605841551e-06},
|
||||
{-1.70612399208458498e-09, 1.07145120553653328e-09, 1.73225882249550267e-09},
|
||||
{1.15369127445807962e-09, -5.80362996549510513e-09, 9.33515653667171819e-10},
|
||||
{3.38692740520230018e-09, 3.72531013675958533e-09, -3.18062756687886861e-09},
|
||||
{1.14787653780236421e-06, -1.84917201319622368e-06, -2.44834286920736499e-07},
|
||||
{1.45558928799083276e-09, 1.12720083267348059e-09, 9.00940544390493869e-10},
|
||||
{2.09654001104286891e-09, 4.92913422578400429e-09, 3.04938074791039071e-10},
|
||||
{3.54033623213741155e-07, 1.07259516691213860e-06, -6.03027205987524684e-07},
|
||||
{-2.72038239157446071e-09, -1.60070143945256760e-09, 6.03853855807301443e-10},
|
||||
{-2.03235662485238069e-06, -1.03151962834260348e-06, 1.99637918628457062e-06},
|
||||
{-1.26261175077493210e-03, -4.98503988506484859e-04, -1.03875859619143593e-03},
|
||||
{6.43182729298530376e-10, 8.01776645076301975e-10, -1.83589794755523172e-09},
|
||||
{4.01805119037978997e-09, -5.63673552278487477e-10, -1.09102650663883693e-08},
|
||||
{-1.48648961195707585e-09, 5.01067861508053269e-09, 2.99132781045319263e-09},
|
||||
{-8.91404754824534629e-07, 7.49163968581634775e-07, 2.12542215183124383e-06},
|
||||
{2.38642574451608525e-09, -3.47605810802065207e-09, 3.86935566920598717e-10},
|
||||
{-2.80031986488182838e-09, -4.25160427697246490e-11, 2.24182921879090280e-09},
|
||||
{-1.26991357818351247e-07, -1.45348284568834647e-07, 5.68792533226815389e-07},
|
||||
{1.39227229745131353e-09, -1.84849578699353145e-09, 2.24967258190267305e-09},
|
||||
{-1.15462500328497586e-06, 1.84347590761761086e-06, 3.64918716654494962e-07},
|
||||
{-2.09357112083411985e-03, 1.60820400301404873e-05, 2.27418117008655948e-04},
|
||||
{-1.04484803378768198e-08, 4.86043558178828050e-09, 2.00996588123336650e-09},
|
||||
{1.44040971927772432e-08, 1.42223015309195233e-09, 1.99778974613318283e-09},
|
||||
{-1.62414574166394599e-07, -1.31976785339561840e-06, 4.43918084507000099e-07},
|
||||
{3.73061943836905385e-09, 1.00036822436866402e-08, -1.05450977117005351e-09},
|
||||
{-2.06551932971539565e-07, -9.72167971235462190e-07, 4.28861904300768815e-07},
|
||||
{-2.16051814014425313e-03, 1.48780488507118812e-03, 7.79940397419977911e-04},
|
||||
{-4.80544204428667854e-09, -1.09870773590259319e-09, 6.58876991984844174e-09},
|
||||
{1.31575045692056136e-06, 4.32430764481131318e-07, -1.55255090541518703e-06},
|
||||
{1.28823975640215602e-03, 4.04521283440268135e-04, 1.76186984141882253e-03},
|
||||
{-1.09767251093991436e-01, -4.94112205838347640e-02, -5.43102978164306804e-02},
|
||||
{7.93691223854864347e-10, 1.54639511196208446e-08, -1.71518303448969789e-08},
|
||||
{2.56523843833456056e-09, -2.31047392329486456e-09, -4.29758133398648601e-09},
|
||||
{-9.87725901069325118e-09, 4.28127375218245732e-09, 2.02888056355376989e-09},
|
||||
{3.21762172461603768e-10, -5.82937505211322815e-09, 3.88293127512318037e-09},
|
||||
{1.63250610252241302e-09, -7.02161705168347083e-09, 3.46592492032893329e-09},
|
||||
{-1.44272117683086343e-07, -4.40408510988914148e-07, 5.92746408872857344e-07},
|
||||
{2.71961467235293242e-09, -1.47466668633244868e-08, 2.89637452632884873e-08},
|
||||
{1.47637712476396399e-08, 1.16406781783262581e-09, 2.04904540557215853e-09},
|
||||
{-5.53709807865621073e-09, 7.05512286092169205e-09, 1.56159114805820565e-09},
|
||||
{5.29268649740455288e-09, 2.10616986628942016e-08, -3.03219004488264332e-08},
|
||||
{1.79978890693655025e-07, 7.95085399132693105e-07, -4.78366567607801940e-07},
|
||||
{-4.03847393894152251e-10, 2.90357085597214848e-09, 1.12992165623992946e-09},
|
||||
{2.99031871486832301e-09, -1.37951879780606745e-09, 2.41048263988075107e-09},
|
||||
{1.26882357398550027e-09, 1.30631467101793852e-09, 7.99574240151201820e-10},
|
||||
{-1.41169562567489137e-08, 1.27148955713198356e-06, -2.89386439707162157e-07},
|
||||
{-2.68794415198003733e-09, 8.73673404455654889e-10, 2.89557382238125882e-09},
|
||||
{-4.90264437380538709e-09, 1.89207244316591527e-09, 2.25393465003165261e-09},
|
||||
{-3.58274654665979853e-08, 2.91386646529383231e-07, -4.98477764412919022e-08},
|
||||
{1.65722165851311942e-09, -1.11673743863338615e-09, -4.14131162695952071e-09},
|
||||
{-1.47751280626939874e-07, -2.41471865000848773e-07, -8.53552350049691100e-07},
|
||||
{-2.24352957583577790e-04, 1.60900273524284708e-03, -1.32260753549593617e-03},
|
||||
{2.05497643901431104e-09, 1.38702982710459111e-08, -3.09887516689033582e-09},
|
||||
{3.39770491949997755e-09, 9.41613393506957053e-09, -7.09844738544518350e-10},
|
||||
{7.86209687630989862e-10, 1.93556837224662104e-10, -6.58630930350234678e-09},
|
||||
{-6.86841181152253455e-10, -5.57194149153339424e-09, 1.41214109156129197e-09},
|
||||
{2.59516074158083754e-07, 1.30703181255419770e-06, -4.02454784192984860e-07},
|
||||
{-5.79425202262839889e-10, 4.05071760856134944e-09, 3.02384985106929349e-09},
|
||||
{4.00677924866643664e-09, -2.25614611715219127e-09, 7.52819043214891792e-09},
|
||||
{2.34003759425061020e-09, 5.27462258592681366e-09, -2.05723854618256041e-10},
|
||||
{2.29340174767722615e-07, 1.05507868574435809e-06, -4.45904844964539748e-07},
|
||||
{-3.91634245866523401e-09, 1.07849931763048801e-09, 1.85542686770290288e-09},
|
||||
{-6.62166513287765213e-09, 3.86355018811013196e-09, -1.87861701195224384e-09},
|
||||
{1.32112240848469842e-07, 4.39339645861430705e-08, -1.59384598983486336e-06},
|
||||
{2.02488462108796341e-09, -1.48427112267590644e-09, -4.32055485832805175e-09},
|
||||
{-4.27701540045566375e-07, -1.46229443391283215e-06, -2.38186369433401879e-07},
|
||||
{-9.86744509368740232e-04, 1.91104095070606826e-03, -8.17774843405986713e-04},
|
||||
{2.06891823117949514e-10, -2.64060942556376688e-09, 1.86419366055012858e-09},
|
||||
{8.33785634979378187e-09, -1.00697171434571686e-08, -2.84106664583116952e-09},
|
||||
{5.07057938692323518e-09, -9.56246298811080919e-09, -6.33399999117045809e-11},
|
||||
{-6.78808357162941078e-08, -2.21612941845184680e-07, 9.42031624998063144e-08},
|
||||
{-3.04300065007145903e-09, 5.64120231083542478e-09, 1.65718606892628628e-09},
|
||||
{3.76240642807612602e-09, -4.58941407446844529e-09, 5.06162500801821125e-09},
|
||||
{7.25149885354159363e-07, -1.18149759075966698e-06, -6.82406347277120240e-07},
|
||||
{-4.84358128605144600e-09, 4.56893046833772853e-09, 2.67044331092591847e-09},
|
||||
{-2.54939737986958903e-07, -1.06106228658746360e-06, 5.04013386790069795e-07},
|
||||
{-2.17097468872509735e-03, 1.41624400187313607e-03, 8.11305605779899562e-04},
|
||||
{2.24635331169675823e-10, -6.02144184513875302e-09, 4.15827878380570226e-09},
|
||||
{-4.55408258326350790e-09, 6.20319154376325343e-09, 2.08760821823750220e-09},
|
||||
{2.10871853867367065e-07, -4.29346688506603014e-07, 1.15683623843482186e-07},
|
||||
{1.00732072683129559e-09, 3.88267751283422058e-11, -6.73798626615873530e-09},
|
||||
{5.34506627847264326e-09, -8.01262819982717645e-08, 1.60888846226225901e-06},
|
||||
{5.83419066552946048e-04, -2.36474094848551555e-03, 8.79373865688287898e-04},
|
||||
{-4.85158746510450101e-10, -6.78789624508624456e-09, 4.95385649168511577e-09},
|
||||
{3.47485142271342085e-07, 5.60944792101468470e-07, -4.35887910682497548e-07},
|
||||
{5.75824910919892421e-04, -2.18618554413632388e-03, 1.22736498224538170e-03},
|
||||
{-2.51838883195707221e-02, -8.23487774284355212e-02, 3.33658831723806573e-02},
|
||||
{-8.70167529698484543e-09, -1.37080219501928280e-08, 1.80728228771354082e-08},
|
||||
{-4.67111571644807100e-09, -2.72041008123058425e-09, 7.06648883852523113e-09},
|
||||
{7.26183221906172727e-10, -6.77816339167414128e-09, 4.52883232651690726e-09},
|
||||
{5.28852302228433047e-09, 6.47161005340457507e-09, -8.67298467766008940e-09},
|
||||
{-2.25465519365641853e-07, -6.46057585221293529e-07, 3.48151143400587948e-07},
|
||||
{-1.30051025504229756e-09, -3.25062288891730944e-09, 2.01775679498084060e-09},
|
||||
{-5.12724809831333062e-09, 9.33902577666956280e-10, -6.96327353416625883e-10},
|
||||
{-3.10810940873373909e-09, -7.49756534634826721e-10, 6.87357185058523612e-10},
|
||||
{-1.52109221995821997e-06, -4.22908767925417317e-07, 1.38629667568307413e-06},
|
||||
{1.42955317028459206e-09, -7.02968461219199980e-10, -3.81617160094549490e-09},
|
||||
{2.53707400921232562e-09, -1.60727622877665510e-09, -4.18765366827500429e-09},
|
||||
{-2.14750738948554787e-07, -6.40554276953864132e-07, 3.76128531993924486e-07},
|
||||
{3.83073214815787821e-09, 4.50296289838947317e-10, 2.29523194894554194e-09},
|
||||
{4.76340728555735282e-07, 6.83235613037347367e-07, -4.72205395646296822e-07},
|
||||
{-6.10651996176347607e-04, -1.06790499934057291e-03, 2.29083496655867842e-03},
|
||||
{3.95497823379997726e-09, 1.38236928154400474e-09, -6.26218820548585242e-09},
|
||||
{1.11904936705986557e-09, -1.37869946362223494e-08, -9.34049783699042457e-10},
|
||||
{1.25499246411697740e-09, -2.73635453185150368e-09, -2.91506864740637139e-09},
|
||||
{-3.59882924006599270e-07, 1.32511373732895413e-06, -1.55110207063907657e-07},
|
||||
{1.07068498511608823e-09, 8.92087770321126072e-09, 2.62826524433101838e-10},
|
||||
{-2.69316546841480431e-09, 9.61138280075601870e-10, 5.19946977139973399e-09},
|
||||
{-5.92563579700916554e-07, -1.05071339539294234e-06, 1.56249964602256375e-07},
|
||||
{1.32198180180509439e-09, 5.16087961255351502e-09, 8.46339526239248130e-10},
|
||||
{2.07323220008381881e-06, 1.02309267446332522e-06, -2.07661522726165781e-06},
|
||||
{1.31402366846389393e-03, 3.78229792813366064e-04, 1.77496793932758741e-03},
|
||||
{8.59301428624004160e-10, -6.83071707530125138e-09, 3.36249680876754553e-09},
|
||||
{5.27310424491833629e-09, 2.09999085065692981e-08, -3.10459945807028959e-08},
|
||||
{-8.88666080375855039e-08, 4.60897593930476024e-07, 7.41576575386676540e-07},
|
||||
{-4.85540663230921155e-10, -5.58243438975036810e-09, 7.40450811775872353e-10},
|
||||
{4.03141117225058743e-07, 1.52035531639227450e-06, 9.06206514897367477e-08},
|
||||
{5.61075629915620496e-04, -2.05847905628765053e-03, 1.12849817492909434e-03},
|
||||
{5.11216541321246609e-09, 7.26292920250060092e-09, -8.97145741030058730e-09},
|
||||
{-4.26211688914213127e-07, -7.03366608210270750e-07, 6.27995585866791828e-07},
|
||||
{1.15309052943982646e-03, 2.34474318844151959e-03, -4.91856748507475423e-03},
|
||||
{1.01104427799588961e-01, -4.22361682938472982e-02, -1.88750007538552200e-01},
|
||||
{3.94738332298860684e-10, -7.81372397340440727e-10, 4.06815717224340290e-09},
|
||||
{-8.61483928638051566e-09, 5.37427180535843263e-09, 1.81738104426676372e-08},
|
||||
{-8.48011268844706123e-10, -5.33803143354383280e-09, 2.99703953494934172e-10},
|
||||
{3.89154099408092063e-07, -2.44166311268514957e-07, -8.03240371135063858e-07},
|
||||
{-1.20249536439409610e-08, 1.48908931921210019e-08, 1.88292573199966284e-09},
|
||||
{-1.16401289163015065e-08, 2.57866422936903206e-08, -5.27022399332555125e-09},
|
||||
{1.37065399911928676e-07, 2.16494406102361175e-08, -7.63924557662179482e-07},
|
||||
{-6.94754161319199870e-10, 6.65038621394664631e-09, -4.31779645371221932e-09},
|
||||
{4.72542155592614588e-07, -7.58546986886782931e-07, -2.35913417925837088e-07},
|
||||
{1.46133817312113241e-03, -3.25193103208258009e-04, -3.06625181254991741e-04},
|
||||
{9.35794082672593210e-09, -7.92923574022275091e-09, -5.41426242728348939e-09},
|
||||
{-2.15279239157428748e-08, -4.16754339024882903e-09, -1.12896482995505920e-08},
|
||||
{2.60645369870582400e-10, 1.44616071127263122e-06, -3.63334053799999057e-07},
|
||||
{9.17105741349288905e-09, -2.02295233654725681e-08, -1.20002956877085509e-08},
|
||||
{-1.27759226226098477e-07, 1.28193771791124470e-06, -5.83097827522305323e-07},
|
||||
{2.26880791869919426e-03, -1.34042850080092401e-03, -7.65092051285704835e-04},
|
||||
{7.03374036792325796e-09, -2.53508958270032281e-09, -7.66132998708535240e-09},
|
||||
{-9.71978722189015265e-07, -5.57836512454779054e-07, 1.96329328074063003e-06},
|
||||
{-1.26115140811304343e-03, -4.81792074617704632e-04, -1.06803272537897391e-03},
|
||||
{1.19419564863885497e-01, 5.07766738901840875e-02, 4.87642090320925953e-02},
|
||||
{1.14090414893297520e-09, 1.56073433760228752e-08, -1.78054684078429726e-08},
|
||||
{3.03285130343056153e-09, -1.58615337531031741e-09, -4.94928394101368241e-09},
|
||||
{2.64483280249840080e-07, 2.97155396291660413e-07, -5.41608085095034164e-07},
|
||||
{2.68757552324139226e-09, -1.41400907649469332e-08, 2.93255796729452456e-08},
|
||||
{-2.11094617584561828e-07, -6.56355695552793272e-07, 3.72180321686621518e-07},
|
||||
{-2.55073452371079590e-04, 1.57943859317488818e-03, -1.29154484940938240e-03},
|
||||
{1.40049266628139435e-09, 1.40747080656922208e-08, -2.58792021839981956e-09},
|
||||
{-2.12330362681090179e-07, -1.30522733223815968e-06, 5.84417623253341567e-07},
|
||||
{-9.33144849909676392e-04, 1.90305575962152547e-03, -8.35564417983726418e-04},
|
||||
{1.81624805201406961e-02, 6.84911174969819458e-02, -2.28291882522520390e-02},
|
||||
{-8.25231299961259879e-09, -1.40227519596081152e-08, 1.78809529925716415e-08},
|
||||
{1.90689491530449118e-07, 7.01057736002264065e-07, -4.26430629252294580e-07},
|
||||
{-5.85146839837499930e-04, -1.07311215649546045e-03, 2.31986890222730339e-03},
|
||||
{-1.05962397073886522e-01, 5.51532131360410807e-02, 1.87542648909451215e-01},
|
||||
{-1.37499370823599516e-03, -8.49619409242363438e-04, -1.18180356709159952e-03}
|
||||
};
|
||||
|
||||
static const double INTERCEPT[3] = {
|
||||
-1.29208772400146188e+00,
|
||||
6.62251952866635918e+00,
|
||||
-1.35908984683965173e-01
|
||||
};
|
||||
// END AUTO-GENERATED COEFFICIENTS
|
||||
|
||||
inline void compute_poly_features(const double x[7], double out[330]) {
|
||||
for (int i = 0; i < N_FEATURES; ++i) {
|
||||
double val = 1.0;
|
||||
for (int j = 0; j < N_INPUTS; ++j) {
|
||||
if (POWERS[i][j] != 0) {
|
||||
double base = x[j];
|
||||
int exp = POWERS[i][j];
|
||||
// Fast integer exponentiation (max exp = 4)
|
||||
double p = 1.0;
|
||||
for (int e = 0; e < exp; ++e)
|
||||
p *= base;
|
||||
val *= p;
|
||||
}
|
||||
}
|
||||
out[i] = val;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
struct RGB {
|
||||
unsigned char r, g, b;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mix two RGB colors using polynomial pigment mixing.
|
||||
*
|
||||
* This performs polynomial pigment-style RGB interpolation.
|
||||
*
|
||||
* @param r1,g1,b1 First color (0-255)
|
||||
* @param r2,g2,b2 Second color (0-255)
|
||||
* @param t Mixing ratio: 0.0 = all color1, 1.0 = all color2
|
||||
* @param out_r,out_g,out_b Output color (0-255)
|
||||
*/
|
||||
inline void lerp(unsigned char r1, unsigned char g1, unsigned char b1,
|
||||
unsigned char r2, unsigned char g2, unsigned char b2,
|
||||
float t,
|
||||
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) {
|
||||
// Clamp t
|
||||
if (t <= 0.0f) {
|
||||
*out_r = r1; *out_g = g1; *out_b = b1;
|
||||
return;
|
||||
}
|
||||
if (t >= 1.0f) {
|
||||
*out_r = r2; *out_g = g2; *out_b = b2;
|
||||
return;
|
||||
}
|
||||
|
||||
double x[7] = {
|
||||
static_cast<double>(r1), static_cast<double>(g1), static_cast<double>(b1),
|
||||
static_cast<double>(r2), static_cast<double>(g2), static_cast<double>(b2),
|
||||
static_cast<double>(t)
|
||||
};
|
||||
|
||||
double features[330];
|
||||
detail::compute_poly_features(x, features);
|
||||
|
||||
// Dot product: features @ COEF + INTERCEPT
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
double sum = detail::INTERCEPT[c];
|
||||
for (int i = 0; i < detail::N_FEATURES; ++i) {
|
||||
sum += features[i] * detail::COEF[i][c];
|
||||
}
|
||||
// Clamp to [0, 255] and truncate (matches numpy astype(int) behavior)
|
||||
int val = static_cast<int>(sum);
|
||||
if (val < 0) val = 0;
|
||||
if (val > 255) val = 255;
|
||||
|
||||
if (c == 0) *out_r = static_cast<unsigned char>(val);
|
||||
else if (c == 1) *out_g = static_cast<unsigned char>(val);
|
||||
else *out_b = static_cast<unsigned char>(val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience overload returning an RGB struct.
|
||||
*/
|
||||
inline RGB lerp(unsigned char r1, unsigned char g1, unsigned char b1,
|
||||
unsigned char r2, unsigned char g2, unsigned char b2,
|
||||
float t) {
|
||||
RGB result;
|
||||
lerp(r1, g1, b1, r2, g2, b2, t, &result.r, &result.g, &result.b);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace filament_mixer
|
||||
|
||||
#endif // FILAMENT_MIXER_MODEL_HPP
|
||||
@@ -108,6 +108,22 @@ const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
|
||||
return m_cached_coefficients;
|
||||
}
|
||||
|
||||
bool CornerSmoother::is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next)
|
||||
{
|
||||
const Vec2d incoming_leg = vertex - previous;
|
||||
const Vec2d outgoing_leg = next - vertex;
|
||||
const double incoming_length = incoming_leg.norm();
|
||||
const double outgoing_length = outgoing_leg.norm();
|
||||
// A vertex repeating one of its neighbours carries no direction of its own.
|
||||
if (incoming_length < EPSILON || outgoing_length < EPSILON)
|
||||
return true;
|
||||
|
||||
const Vec2d incoming = incoming_leg / incoming_length;
|
||||
const Vec2d outgoing = outgoing_leg / outgoing_length;
|
||||
return incoming.dot(outgoing) > 0. &&
|
||||
std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x()) < EPSILON;
|
||||
}
|
||||
|
||||
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
|
||||
{
|
||||
m_corner_points.clear();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
@@ -47,36 +48,57 @@ public:
|
||||
|
||||
template<typename Emit> void push(const Vec2d &point, Emit &emit)
|
||||
{
|
||||
if (m_pending == 0) {
|
||||
if (m_held == 0) {
|
||||
// The first point of a path is an end, not a corner, and stays where it is.
|
||||
emit(point);
|
||||
m_previous = point;
|
||||
} else if (m_pending > 1) {
|
||||
round_corner(m_previous, m_corner, point);
|
||||
for (const Vec2d &corner_point : m_corner_points)
|
||||
emit(corner_point);
|
||||
m_previous = m_corner;
|
||||
m_window[m_held++] = point;
|
||||
return;
|
||||
}
|
||||
m_corner = point;
|
||||
m_pending = std::min(m_pending + 1, 2);
|
||||
if (m_held > 1 && is_on_straight_run(m_window[m_held - 2], m_window[m_held - 1], point)) {
|
||||
// The newest vertex only splits a straight leg, so the leg runs on to this point instead.
|
||||
m_window[m_held - 1] = point;
|
||||
return;
|
||||
}
|
||||
if (m_held < 3) {
|
||||
m_window[m_held++] = point;
|
||||
return;
|
||||
}
|
||||
// Both legs of the middle vertex are complete now, so its curve can no longer grow.
|
||||
emit_corner(m_window[0], m_window[1], m_window[2], emit);
|
||||
m_window[0] = m_window[1];
|
||||
m_window[1] = m_window[2];
|
||||
m_window[2] = point;
|
||||
}
|
||||
|
||||
// Emits the last point of the path and prepares the smoother for a new one.
|
||||
template<typename Emit> void flush(Emit &emit)
|
||||
{
|
||||
if (m_pending > 1)
|
||||
emit(m_corner);
|
||||
m_pending = 0;
|
||||
if (m_held > 2)
|
||||
emit_corner(m_window[0], m_window[1], m_window[2], emit);
|
||||
if (m_held > 1)
|
||||
emit(m_window[m_held - 1]);
|
||||
m_held = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename Emit> void emit_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next, Emit &emit)
|
||||
{
|
||||
round_corner(previous, corner, next);
|
||||
for (const Vec2d &corner_point : m_corner_points)
|
||||
emit(corner_point);
|
||||
}
|
||||
|
||||
// Tells a vertex that only continues a straight leg (or repeats its predecessor) from a corner.
|
||||
// A path doubling back on itself is not one, that vertex is a hairpin and stays where it is.
|
||||
static bool is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next);
|
||||
// Fills m_corner_points with the points replacing the corner vertex.
|
||||
void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next);
|
||||
// Flattens the canonical corner curve of the given size and turn into coordinates of the
|
||||
// (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner.
|
||||
const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing);
|
||||
|
||||
// Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment
|
||||
// is the maximum, otherwise the curves of two adjacent corners would overlap.
|
||||
// Fraction of the shorter adjoining leg consumed on each side of a corner. Half of a leg is the
|
||||
// maximum, otherwise the curves of two adjacent corners would overlap.
|
||||
const double m_corner_distance_ratio;
|
||||
const double m_tolerance;
|
||||
const double m_max_corner_distance;
|
||||
@@ -88,10 +110,11 @@ private:
|
||||
double m_cached_cosine { 0. };
|
||||
bool m_has_cached_coefficients { false };
|
||||
|
||||
Vec2d m_previous { Vec2d::Zero() };
|
||||
Vec2d m_corner { Vec2d::Zero() };
|
||||
// Number of points held back: none, the first point of a path, or a corner candidate.
|
||||
int m_pending { 0 };
|
||||
// The corners seen last, kept free of vertices that merely split a straight leg. The middle one
|
||||
// is rounded once the third arrives, which is what makes its outgoing leg final.
|
||||
std::array<Vec2d, 3> m_window { Vec2d::Zero(), Vec2d::Zero(), Vec2d::Zero() };
|
||||
// How many of them are filled in.
|
||||
int m_held { 0 };
|
||||
};
|
||||
|
||||
// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone.
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
#include "AssimpImport.hpp"
|
||||
|
||||
#include "../TexturePainting.hpp"
|
||||
#include "ResourcePathUtils.hpp"
|
||||
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/config.h>
|
||||
#include <assimp/material.h>
|
||||
#include <assimp/postprocess.h>
|
||||
#include <assimp/scene.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
void clear_textured_mesh(TexturedMesh& out)
|
||||
{
|
||||
out.vertices.clear();
|
||||
out.indices.clear();
|
||||
out.uvs.clear();
|
||||
out.uv_coords.clear();
|
||||
out.uv_indices.clear();
|
||||
out.textures.clear();
|
||||
out.material_ids.clear();
|
||||
out.material_texture_map.clear();
|
||||
out.material_colors.clear();
|
||||
}
|
||||
|
||||
void set_error_message(std::string* error_message, const std::string& message)
|
||||
{
|
||||
if (error_message)
|
||||
*error_message = message;
|
||||
}
|
||||
|
||||
bool is_fbx_path(const std::string& path)
|
||||
{
|
||||
return boost::algorithm::iends_with(path, ".fbx");
|
||||
}
|
||||
|
||||
bool should_flip_uvs(const std::string& path)
|
||||
{
|
||||
return boost::algorithm::iends_with(path, ".fbx") ||
|
||||
boost::algorithm::iends_with(path, ".glb");
|
||||
}
|
||||
|
||||
unsigned int assimp_import_flags(const std::string& path)
|
||||
{
|
||||
unsigned int flags = aiProcess_Triangulate
|
||||
| aiProcess_GenNormals
|
||||
| aiProcess_PreTransformVertices
|
||||
| aiProcess_SortByPType;
|
||||
if (should_flip_uvs(path))
|
||||
flags |= aiProcess_FlipUVs;
|
||||
return flags;
|
||||
}
|
||||
|
||||
void configure_importer(Assimp::Importer& importer, const std::string& path, unsigned int flags)
|
||||
{
|
||||
importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE,
|
||||
aiPrimitiveType_POINT | aiPrimitiveType_LINE);
|
||||
|
||||
if (flags & aiProcess_PreTransformVertices)
|
||||
importer.SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true);
|
||||
|
||||
if (is_fbx_path(path)) {
|
||||
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true);
|
||||
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true);
|
||||
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true);
|
||||
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, false);
|
||||
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
|
||||
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool read_external_texture_file(const boost::filesystem::path& path, TextureImage& out)
|
||||
{
|
||||
boost::nowide::ifstream file(path.string(), std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open())
|
||||
return false;
|
||||
|
||||
const std::streamoff size = file.tellg();
|
||||
if (size <= 0)
|
||||
return false;
|
||||
if (static_cast<uintmax_t>(size) > static_cast<uintmax_t>(std::numeric_limits<size_t>::max()))
|
||||
return false;
|
||||
|
||||
file.seekg(0);
|
||||
out.width = -1;
|
||||
out.height = -1;
|
||||
out.channels = 0;
|
||||
out.data.resize(static_cast<size_t>(size));
|
||||
file.read(reinterpret_cast<char*>(out.data.data()), size);
|
||||
if (!file && !file.eof()) {
|
||||
out.data.clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_embedded_texture(const aiTexture& texture, TextureImage& out)
|
||||
{
|
||||
out.data.clear();
|
||||
if (texture.mHeight == 0) {
|
||||
if (texture.mWidth == 0)
|
||||
return false;
|
||||
out.width = -1;
|
||||
out.height = -1;
|
||||
out.channels = 0;
|
||||
out.data.assign(
|
||||
reinterpret_cast<const unsigned char*>(texture.pcData),
|
||||
reinterpret_cast<const unsigned char*>(texture.pcData) + texture.mWidth);
|
||||
return !out.data.empty();
|
||||
}
|
||||
|
||||
if (texture.mWidth == 0 || texture.mHeight == 0)
|
||||
return false;
|
||||
if (texture.mWidth > static_cast<unsigned int>(std::numeric_limits<int>::max()) ||
|
||||
texture.mHeight > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
|
||||
return false;
|
||||
}
|
||||
const size_t width = static_cast<size_t>(texture.mWidth);
|
||||
const size_t height = static_cast<size_t>(texture.mHeight);
|
||||
if (width > std::numeric_limits<size_t>::max() / height ||
|
||||
width * height > std::numeric_limits<size_t>::max() / 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = static_cast<int>(texture.mWidth);
|
||||
out.height = static_cast<int>(texture.mHeight);
|
||||
out.channels = 4;
|
||||
const size_t pixel_count = width * height;
|
||||
out.data.resize(pixel_count * 4);
|
||||
for (size_t i = 0; i < pixel_count; ++i) {
|
||||
const aiTexel& texel = texture.pcData[i];
|
||||
out.data[i * 4 + 0] = texel.r;
|
||||
out.data[i * 4 + 1] = texel.g;
|
||||
out.data[i * 4 + 2] = texel.b;
|
||||
out.data[i * 4 + 3] = texel.a;
|
||||
}
|
||||
return !out.data.empty();
|
||||
}
|
||||
|
||||
bool get_material_texture(const aiMaterial& material, aiString& texture_path)
|
||||
{
|
||||
if (material.GetTextureCount(aiTextureType_DIFFUSE) > 0 &&
|
||||
material.GetTexture(aiTextureType_DIFFUSE, 0, &texture_path) == AI_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (material.GetTextureCount(aiTextureType_BASE_COLOR) > 0 &&
|
||||
material.GetTexture(aiTextureType_BASE_COLOR, 0, &texture_path) == AI_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<float, 4> get_material_color(const aiMaterial& material)
|
||||
{
|
||||
aiColor4D color(1.f, 1.f, 1.f, 1.f);
|
||||
if (material.Get(AI_MATKEY_BASE_COLOR, color) == AI_SUCCESS)
|
||||
return {color.r, color.g, color.b, color.a};
|
||||
if (material.Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS)
|
||||
return {color.r, color.g, color.b, color.a};
|
||||
return {1.f, 1.f, 1.f, 1.f};
|
||||
}
|
||||
|
||||
bool collect_mesh(const aiMesh& mesh, size_t& vertex_offset, TexturedMesh& out, std::string& error)
|
||||
{
|
||||
if (mesh.mNumVertices > static_cast<size_t>(std::numeric_limits<int>::max()) - vertex_offset) {
|
||||
error = "Assimp mesh has too many vertices for TexturedMesh indices";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < mesh.mNumVertices; ++i) {
|
||||
const aiVector3D& v = mesh.mVertices[i];
|
||||
out.vertices.push_back({v.x, v.y, v.z});
|
||||
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
const aiVector3D& uv = mesh.mTextureCoords[0][i];
|
||||
out.uvs.push_back({uv.x, uv.y});
|
||||
} else {
|
||||
out.uvs.push_back({0.f, 0.f});
|
||||
}
|
||||
}
|
||||
|
||||
const int material_index = static_cast<int>(mesh.mMaterialIndex);
|
||||
for (unsigned int i = 0; i < mesh.mNumFaces; ++i) {
|
||||
const aiFace& face = mesh.mFaces[i];
|
||||
if (face.mNumIndices != 3)
|
||||
continue;
|
||||
if (face.mIndices[0] >= mesh.mNumVertices ||
|
||||
face.mIndices[1] >= mesh.mNumVertices ||
|
||||
face.mIndices[2] >= mesh.mNumVertices) {
|
||||
error = "Assimp mesh face index is out of bounds";
|
||||
return false;
|
||||
}
|
||||
out.indices.push_back({
|
||||
static_cast<int>(static_cast<size_t>(face.mIndices[0]) + vertex_offset),
|
||||
static_cast<int>(static_cast<size_t>(face.mIndices[1]) + vertex_offset),
|
||||
static_cast<int>(static_cast<size_t>(face.mIndices[2]) + vertex_offset)});
|
||||
out.material_ids.push_back(material_index);
|
||||
}
|
||||
|
||||
vertex_offset += mesh.mNumVertices;
|
||||
return true;
|
||||
}
|
||||
|
||||
void collect_materials(const aiScene& scene, const boost::filesystem::path& base_dir, TexturedMesh& out)
|
||||
{
|
||||
out.material_texture_map.assign(scene.mNumMaterials, -1);
|
||||
out.material_colors.assign(scene.mNumMaterials, {1.f, 1.f, 1.f, 1.f});
|
||||
|
||||
for (unsigned int material_index = 0; material_index < scene.mNumMaterials; ++material_index) {
|
||||
const aiMaterial* material = scene.mMaterials[material_index];
|
||||
if (!material)
|
||||
continue;
|
||||
|
||||
out.material_colors[material_index] = get_material_color(*material);
|
||||
|
||||
aiString texture_path;
|
||||
if (!get_material_texture(*material, texture_path))
|
||||
continue;
|
||||
|
||||
TextureImage image;
|
||||
const aiTexture* embedded_texture = scene.GetEmbeddedTexture(texture_path.C_Str());
|
||||
if (embedded_texture) {
|
||||
if (!read_embedded_texture(*embedded_texture, image))
|
||||
continue;
|
||||
} else {
|
||||
const boost::filesystem::path resolved = resource_path::resolve_external_resource_path(
|
||||
base_dir, texture_path.C_Str(), "Assimp texture");
|
||||
if (resolved.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "AssimpImport: texture file not found: "
|
||||
<< texture_path.C_Str();
|
||||
continue;
|
||||
}
|
||||
if (!read_external_texture_file(resolved, image)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "AssimpImport: failed to read texture: "
|
||||
<< resolved;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
out.material_texture_map[material_index] = static_cast<int>(out.textures.size());
|
||||
out.textures.push_back(std::move(image));
|
||||
}
|
||||
}
|
||||
|
||||
std::string scene_failure_summary(const std::string& path, const char* assimp_error)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << "Assimp failed to import " << path;
|
||||
if (assimp_error && assimp_error[0] != '\0')
|
||||
ss << ": " << assimp_error;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message)
|
||||
{
|
||||
clear_textured_mesh(out);
|
||||
|
||||
Assimp::Importer importer;
|
||||
const unsigned int flags = assimp_import_flags(path);
|
||||
configure_importer(importer, path, flags);
|
||||
|
||||
const aiScene* scene = importer.ReadFile(path, flags);
|
||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
||||
const std::string message = scene_failure_summary(path, importer.GetErrorString());
|
||||
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
|
||||
set_error_message(error_message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scene->mNumMeshes == 0) {
|
||||
const std::string message = "Assimp scene has no meshes: " + path;
|
||||
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
|
||||
set_error_message(error_message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t vertex_offset = 0;
|
||||
for (unsigned int mesh_index = 0; mesh_index < scene->mNumMeshes; ++mesh_index) {
|
||||
const aiMesh* mesh = scene->mMeshes[mesh_index];
|
||||
if (!mesh || !mesh->HasPositions())
|
||||
continue;
|
||||
std::string mesh_error;
|
||||
if (!collect_mesh(*mesh, vertex_offset, out, mesh_error)) {
|
||||
const std::string message = mesh_error + ": " + path;
|
||||
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
|
||||
set_error_message(error_message, message);
|
||||
clear_textured_mesh(out);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (out.vertices.empty() || out.indices.empty()) {
|
||||
const std::string message = "Assimp extracted no valid triangles: " + path;
|
||||
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
|
||||
set_error_message(error_message, message);
|
||||
clear_textured_mesh(out);
|
||||
return false;
|
||||
}
|
||||
|
||||
collect_materials(*scene, boost::filesystem::path(path).parent_path(), out);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "AssimpImport: loaded " << out.vertices.size()
|
||||
<< " vertices, " << out.indices.size()
|
||||
<< " triangles, " << out.textures.size()
|
||||
<< " textures from " << path;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct TexturedMesh;
|
||||
|
||||
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -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"
|
||||
@@ -8,6 +10,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define DIR_SEPARATOR '\\'
|
||||
@@ -21,7 +24,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 +101,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 +214,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 +239,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.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef slic3r_Format_OBJ_hpp_
|
||||
#define slic3r_Format_OBJ_hpp_
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "objparser.hpp"
|
||||
#include <unordered_map>
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -18,6 +19,7 @@ struct ObjInfo {
|
||||
std::map<std::string,bool> pngs;
|
||||
std::unordered_map<int, std::string> uv_map_pngs;
|
||||
bool has_uv_png{false};
|
||||
std::vector<ObjParser::ObjUseMtl> usemtls; // material spans, for texture import
|
||||
|
||||
};
|
||||
struct ObjDialogInOut
|
||||
@@ -32,8 +34,18 @@ struct ObjDialogInOut
|
||||
std::string lost_material_name{""};
|
||||
};
|
||||
typedef std::function<void(ObjDialogInOut &in_out)> ObjImportColorFn;
|
||||
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message);
|
||||
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr);
|
||||
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message, ObjParser::MtlData *out_mtl = nullptr);
|
||||
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr, ObjParser::MtlData *out_mtl = nullptr);
|
||||
|
||||
struct TexturedMesh;
|
||||
// Build a TexturedMesh (vertices + per-face UVs + the texture files named by map_Kd) from a
|
||||
// parsed OBJ plus its material table, so the texture-to-color importer can sample face colours.
|
||||
extern bool obj_to_textured_mesh(
|
||||
const ObjInfo& obj_info,
|
||||
const indexed_triangle_set& its,
|
||||
const ObjParser::MtlData& mtl_data,
|
||||
const std::string& obj_directory,
|
||||
TexturedMesh& out);
|
||||
|
||||
extern bool store_obj(const char *path, TriangleMesh *mesh);
|
||||
extern bool store_obj(const char *path, ModelObject *model);
|
||||
|
||||
@@ -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_ */
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../Preset.hpp"
|
||||
#include "../Utils.hpp"
|
||||
#include "../LocalesUtils.hpp"
|
||||
#include "../FilamentMixer.hpp"
|
||||
#include "../GCode.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
#include "../GCode/ThumbnailData.hpp"
|
||||
@@ -246,6 +247,8 @@ static constexpr const char* BUILD_TAG = "build";
|
||||
static constexpr const char* ITEM_TAG = "item";
|
||||
static constexpr const char* METADATA_TAG = "metadata";
|
||||
static constexpr const char* FILAMENT_TAG = "filament";
|
||||
static constexpr const char* MIXED_FILAMENT_TAG = "mixed_filament";
|
||||
static constexpr const char* MIXED_FILAMENT_COMPONENTS_TAG = "components";
|
||||
static constexpr const char* SLICE_WARNING_TAG = "warning";
|
||||
static constexpr const char* WARNING_MSG_TAG = "msg";
|
||||
static constexpr const char *FILAMENT_ID_TAG = "id";
|
||||
@@ -1334,6 +1337,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
bool _handle_end_config_metadata();
|
||||
|
||||
bool _handle_start_config_filament(const char** attributes, unsigned int num_attributes);
|
||||
bool _handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes);
|
||||
bool _handle_end_config_filament();
|
||||
|
||||
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
|
||||
@@ -2713,6 +2717,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", load project config file successfully from %1%\n") %dest_file;
|
||||
|
||||
// Heal any gradient-curve slots corrupted by the legacy "|" separator collision
|
||||
// (see FilamentMixer::sanitize_mixed_gradient_curve_array). The 3MF JSON itself
|
||||
// is safe (";" + C-style escape), but older projects saved through the buggy
|
||||
// export_selections/load_selections path may already carry single-point entries
|
||||
// that fail MakerWorld's "curve needs >= 2 points" check.
|
||||
if (auto* curve_opt = config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
|
||||
Slic3r::sanitize_mixed_gradient_curve_array(curve_opt->values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3530,6 +3542,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
res = _handle_start_config_plater_instance(attributes, num_attributes);
|
||||
else if (::strcmp(FILAMENT_TAG, name) == 0)
|
||||
res = _handle_start_config_filament(attributes, num_attributes);
|
||||
else if (::strcmp(MIXED_FILAMENT_TAG, name) == 0)
|
||||
res = _handle_start_config_mixed_filament(attributes, num_attributes);
|
||||
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
|
||||
res = _handle_start_config_warning(attributes, num_attributes);
|
||||
else if (::strcmp(NOZZLE_TAG, name) == 0)
|
||||
@@ -4703,6 +4717,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Importer::_handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes)
|
||||
{
|
||||
if (m_curr_plater) {
|
||||
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_ID_TAG);
|
||||
std::string type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TYPE_TAG);
|
||||
std::string color = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_COLOR_TAG);
|
||||
std::string components = bbs_get_attribute_value_string(attributes, num_attributes, MIXED_FILAMENT_COMPONENTS_TAG);
|
||||
PlateMixedFilamentInfo mixed_info;
|
||||
mixed_info.id = atoi(id.c_str());
|
||||
mixed_info.type = type;
|
||||
mixed_info.color = color;
|
||||
mixed_info.components = components;
|
||||
m_curr_plater->mixed_filaments_info.push_back(mixed_info);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Importer::_handle_end_config_filament()
|
||||
{
|
||||
// do nothing
|
||||
@@ -8520,6 +8551,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
<< FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n";
|
||||
}
|
||||
|
||||
// Mixed (virtual) filaments used by this plate. These are resolved to physical
|
||||
// components before g-code statistics, so they are not present in the <filament>
|
||||
// list above and are recorded separately here.
|
||||
for (auto it = plate_data->mixed_filaments_info.begin(); it != plate_data->mixed_filaments_info.end(); it++)
|
||||
{
|
||||
stream << " <" << MIXED_FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id) << "\" "
|
||||
<< FILAMENT_TYPE_TAG << "=\"" << it->type << "\" "
|
||||
<< FILAMENT_COLOR_TAG << "=\"" << it->color << "\" "
|
||||
<< MIXED_FILAMENT_COMPONENTS_TAG << "=\"" << it->components << "\"/>\n";
|
||||
}
|
||||
|
||||
for (auto it = plate_data->warnings.begin(); it != plate_data->warnings.end(); it++) {
|
||||
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
|
||||
}
|
||||
|
||||
@@ -48,6 +48,18 @@ public:
|
||||
};
|
||||
|
||||
|
||||
// Mixed (virtual) filament used by a plate. Mixed filaments are virtual slots that get
|
||||
// resolved to their physical components before g-code statistics, so they never appear in
|
||||
// slice_filaments_info. They are recorded here separately so a plate's mixed-color usage
|
||||
// can be recovered from slice_info.
|
||||
struct PlateMixedFilamentInfo
|
||||
{
|
||||
int id{0}; // 1-based virtual filament slot id
|
||||
std::string type;
|
||||
std::string color; // blended display color, "#RRGGBB"
|
||||
std::string components; // 1-based physical component ids, comma separated, e.g. "1,3"
|
||||
};
|
||||
|
||||
//BBS: define plate data list related structures
|
||||
struct PlateData
|
||||
{
|
||||
@@ -89,6 +101,8 @@ struct PlateData
|
||||
std::string first_layer_time;
|
||||
std::string plate_name;
|
||||
std::vector<FilamentInfo> slice_filaments_info;
|
||||
// Mixed (virtual) filaments used by this plate; empty when no mixed filament is used.
|
||||
std::vector<PlateMixedFilamentInfo> mixed_filaments_info;
|
||||
std::vector<size_t> skipped_objects;
|
||||
DynamicPrintConfig config;
|
||||
bool is_support_used {false};
|
||||
|
||||
@@ -262,12 +262,9 @@ static bool obj_parseline(const char *line, ObjData &data)
|
||||
}
|
||||
face_index_count++;
|
||||
}
|
||||
if (face_index_count == 3) {//tri
|
||||
data.usemtls.back().face_end++;
|
||||
} else if (face_index_count == 4) {//quad
|
||||
data.usemtls.back().face_end++;
|
||||
data.usemtls.back().face_end++;
|
||||
}
|
||||
if (face_index_count >= 3) {
|
||||
data.usemtls.back().face_end += face_index_count - 2;
|
||||
}
|
||||
}
|
||||
vertex.coordIdx = -1;
|
||||
vertex.normalIdx = -1;
|
||||
@@ -374,6 +371,107 @@ static bool obj_parseline(const char *line, ObjData &data)
|
||||
return true;
|
||||
}
|
||||
static std::string cur_mtl_name = "";
|
||||
static bool mtl_is_space(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t' || c == '\r';
|
||||
}
|
||||
|
||||
static const char* mtl_skip_ws(const char *line)
|
||||
{
|
||||
while (mtl_is_space(*line))
|
||||
++line;
|
||||
return line;
|
||||
}
|
||||
|
||||
static const char* mtl_skip_token(const char *line)
|
||||
{
|
||||
while (*line != 0 && !mtl_is_space(*line))
|
||||
++line;
|
||||
return line;
|
||||
}
|
||||
|
||||
static bool mtl_token_equals(const char *begin, const char *end, const char *token)
|
||||
{
|
||||
const size_t len = static_cast<size_t>(end - begin);
|
||||
return strlen(token) == len && strncmp(begin, token, len) == 0;
|
||||
}
|
||||
|
||||
static std::string mtl_trim_value(const char *line)
|
||||
{
|
||||
const char *begin = mtl_skip_ws(line);
|
||||
const char *end = begin + strlen(begin);
|
||||
while (end > begin && mtl_is_space(*(end - 1)))
|
||||
--end;
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
static bool mtl_skip_numeric_token(const char *&line)
|
||||
{
|
||||
const char *begin = mtl_skip_ws(line);
|
||||
if (*begin == 0)
|
||||
return false;
|
||||
char *endptr = 0;
|
||||
strtod(begin, &endptr);
|
||||
if (endptr == begin || (!mtl_is_space(*endptr) && *endptr != 0))
|
||||
return false;
|
||||
line = mtl_skip_ws(endptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool mtl_skip_required_tokens(const char *&line, int count)
|
||||
{
|
||||
for (int i = 0; i < count; ++i) {
|
||||
line = mtl_skip_ws(line);
|
||||
if (*line == 0)
|
||||
return false;
|
||||
line = mtl_skip_token(line);
|
||||
}
|
||||
line = mtl_skip_ws(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string mtl_parse_texture_name(const char *line)
|
||||
{
|
||||
const char *original = mtl_skip_ws(line);
|
||||
const char *current = original;
|
||||
|
||||
while (*current == '-') {
|
||||
const char *option_begin = current;
|
||||
const char *option_end = mtl_skip_token(current);
|
||||
current = option_end;
|
||||
|
||||
if (mtl_token_equals(option_begin, option_end, "-o") ||
|
||||
mtl_token_equals(option_begin, option_end, "-s") ||
|
||||
mtl_token_equals(option_begin, option_end, "-t")) {
|
||||
int skipped = 0;
|
||||
while (skipped < 3 && mtl_skip_numeric_token(current))
|
||||
++skipped;
|
||||
if (skipped == 0)
|
||||
return mtl_trim_value(original);
|
||||
continue;
|
||||
}
|
||||
|
||||
int option_args = -1;
|
||||
if (mtl_token_equals(option_begin, option_end, "-mm"))
|
||||
option_args = 2;
|
||||
else if (mtl_token_equals(option_begin, option_end, "-bm") ||
|
||||
mtl_token_equals(option_begin, option_end, "-boost") ||
|
||||
mtl_token_equals(option_begin, option_end, "-texres") ||
|
||||
mtl_token_equals(option_begin, option_end, "-clamp") ||
|
||||
mtl_token_equals(option_begin, option_end, "-blendu") ||
|
||||
mtl_token_equals(option_begin, option_end, "-blendv") ||
|
||||
mtl_token_equals(option_begin, option_end, "-cc") ||
|
||||
mtl_token_equals(option_begin, option_end, "-imfchan") ||
|
||||
mtl_token_equals(option_begin, option_end, "-type"))
|
||||
option_args = 1;
|
||||
|
||||
if (option_args < 0 || !mtl_skip_required_tokens(current, option_args))
|
||||
return mtl_trim_value(original);
|
||||
}
|
||||
|
||||
return mtl_trim_value(current);
|
||||
}
|
||||
|
||||
static bool mtl_parseline(const char *line, MtlData &data)
|
||||
{
|
||||
if (*line == 0) return true;
|
||||
@@ -394,13 +492,14 @@ static bool mtl_parseline(const char *line, MtlData &data)
|
||||
ObjNewMtl new_mtl;
|
||||
cur_mtl_name = line;
|
||||
data.new_mtl_unmap[cur_mtl_name] = std::make_shared<ObjNewMtl>();
|
||||
data.mtl_orders.emplace_back(cur_mtl_name);
|
||||
break;
|
||||
}
|
||||
case 'm': {
|
||||
if (*(line++) != 'a' || *(line++) != 'p' || *(line++) != '_' || *(line++) != 'K' || *(line++) != 'd') return false;
|
||||
EATWS();
|
||||
if (data.new_mtl_unmap.find(cur_mtl_name) != data.new_mtl_unmap.end()) {
|
||||
data.new_mtl_unmap[cur_mtl_name]->map_Kd = line;
|
||||
data.new_mtl_unmap[cur_mtl_name]->map_Kd = mtl_parse_texture_name(line);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,9 @@ struct MtlData
|
||||
// Version of the data structure for load / store in the private binary format.
|
||||
int version;
|
||||
std::unordered_map<std::string, std::shared_ptr<ObjNewMtl>> new_mtl_unmap;
|
||||
// Material names in declaration order. new_mtl_unmap is unordered, but OBJ material
|
||||
// indices are positional, so texture import needs the original order.
|
||||
std::vector<std::string> mtl_orders;
|
||||
};
|
||||
extern bool objparse(const char *path, ObjData &data);
|
||||
extern bool mtlparse(const char *path, MtlData &data);
|
||||
|
||||
+361
-8
@@ -4195,6 +4195,8 @@ void GCode::export_layer_filaments(GCodeProcessorResult* result)
|
||||
}
|
||||
}
|
||||
|
||||
result->used_mixed_filaments = m_print->get_slice_used_mixed_filaments();
|
||||
|
||||
result->optimal_assignment.clear();
|
||||
result->optimal_assignment.reserve(filament_map.size());
|
||||
for (int nozzle_id : filament_map)
|
||||
@@ -6004,9 +6006,16 @@ LayerResult GCode::process_layer(
|
||||
|
||||
const WipingExtrusions::ExtruderPerCopy *entity_overrides = nullptr;
|
||||
if (! layer_tools.has_extruder(correct_extruder_id)) {
|
||||
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
|
||||
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
|
||||
correct_extruder_id = layer_tools.extruders.back();
|
||||
// A mixed-color slot is absent from layer_tools.extruders by design:
|
||||
// resolve_mixed_filaments() replaced it with its physical components,
|
||||
// and the sublayer block emits its geometry separately. Reassigning it
|
||||
// to the last extruder here would print it in the wrong colour, so only
|
||||
// fall back for genuinely stale (dontcare) extruders.
|
||||
if (!layer_tools.is_mixed_slot(correct_extruder_id)) {
|
||||
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
|
||||
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
|
||||
correct_extruder_id = layer_tools.extruders.back();
|
||||
}
|
||||
}
|
||||
printing_extruders.clear();
|
||||
if (is_anything_overridden && use_overrides) {
|
||||
@@ -6094,7 +6103,16 @@ LayerResult GCode::process_layer(
|
||||
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
|
||||
single_object_instance_idx == size_t(-1) &&
|
||||
print.config().print_order != PrintOrder::AsObjectList;
|
||||
for (unsigned int filament_id : layer_tools.extruders) {
|
||||
// A mixed-color slot is absent from layer_tools.extruders by design: resolve_mixed_filaments()
|
||||
// replaced it with its physical components. Its geometry is still keyed under the slot in
|
||||
// by_extruder though, and the sublayer emitter looks the plan up by slot id, so append the
|
||||
// slots here. Appending rather than merging leaves the flush-optimized order untouched.
|
||||
std::vector<unsigned int> plan_filaments = layer_tools.extruders;
|
||||
for (const auto &grp : layer_tools.mixed_sub_layer_groups)
|
||||
if (std::find(plan_filaments.begin(), plan_filaments.end(), grp.mixed_slot_0based) == plan_filaments.end())
|
||||
plan_filaments.push_back(grp.mixed_slot_0based);
|
||||
|
||||
for (unsigned int filament_id : plan_filaments) {
|
||||
auto objects_by_extruder_it = by_extruder.find(filament_id);
|
||||
if (objects_by_extruder_it == by_extruder.end()) continue;
|
||||
|
||||
@@ -6275,8 +6293,22 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
|
||||
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
|
||||
std::vector<size_t> filament_instances_id;
|
||||
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
|
||||
std::set<size_t> all_label_ids;
|
||||
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first)
|
||||
all_label_ids.insert(instance.label_object_id);
|
||||
// This extruder may also be printing sub-layers on behalf of a mixed slot, whose
|
||||
// instances live under the slot id. Their labels belong in the same skip set, or
|
||||
// exclude-object would not skip that geometry.
|
||||
for (const auto &grp : layer_tools.mixed_sub_layer_groups)
|
||||
for (unsigned int comp : grp.components_0based)
|
||||
if (comp == extruder_id) {
|
||||
auto mit = filament_to_print_instances.find(grp.mixed_slot_0based);
|
||||
if (mit != filament_to_print_instances.end())
|
||||
for (const InstanceToPrint &inst : mit->second.first)
|
||||
all_label_ids.insert(inst.label_object_id);
|
||||
break;
|
||||
}
|
||||
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
|
||||
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
|
||||
}
|
||||
|
||||
@@ -6557,6 +6589,318 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mixed-color sublayer extrusion: if this extruder is a component of a mixed sublayer
|
||||
// group, extrude the mixed slot's geometry at the appropriate sub-Z with scaled flow.
|
||||
// Ported from BambuStudio and adapted to Orca's instance loop and its finer-grained
|
||||
// per-role region filament options.
|
||||
for (const auto &grp : layer_tools.mixed_sub_layer_groups) {
|
||||
int sub_idx = -1;
|
||||
for (size_t k = 0; k < grp.components_0based.size(); ++k) {
|
||||
if (grp.components_0based[k] == extruder_id) {
|
||||
sub_idx = static_cast<int>(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sub_idx < 0)
|
||||
continue;
|
||||
|
||||
auto mixed_instances_it = filament_to_print_instances.find(grp.mixed_slot_0based);
|
||||
if (mixed_instances_it == filament_to_print_instances.end() || mixed_instances_it->second.first.empty())
|
||||
continue;
|
||||
|
||||
double lh = grp.layer_height > 0. ? grp.layer_height : static_cast<double>(height);
|
||||
double cumulative_h = 0.0;
|
||||
for (int i = 0; i < sub_idx; ++i)
|
||||
cumulative_h += grp.sub_heights[i];
|
||||
double default_sub_h = grp.sub_heights[sub_idx];
|
||||
double default_sub_z = print_z - lh + cumulative_h + default_sub_h;
|
||||
|
||||
m_sub_layer_flow_ratio = default_sub_h / lh;
|
||||
m_sub_layer_height = default_sub_h;
|
||||
m_nominal_z = default_sub_z;
|
||||
|
||||
gcode += this->set_extruder(extruder_id, default_sub_z);
|
||||
|
||||
for (InstanceToPrint &instance_to_print : mixed_instances_it->second.first) {
|
||||
const bool use_per_volume = grp.is_gradient
|
||||
&& !grp.per_volume_gradient.empty()
|
||||
&& std::any_of(grp.per_volume_gradient.begin(), grp.per_volume_gradient.end(),
|
||||
[&](const auto &kv) { return kv.first.obj == &instance_to_print.print_object; });
|
||||
|
||||
// --- Shared instance preamble (mirrors Orca's main instance loop) ---
|
||||
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
|
||||
const auto &inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
|
||||
|
||||
bool object_layer_over_raft = layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 &&
|
||||
instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id();
|
||||
m_config.apply(print.default_region_config());
|
||||
m_config.apply(instance_to_print.print_object.config(), true);
|
||||
m_layer = layer_to_print.layer();
|
||||
m_object_layer_over_raft = object_layer_over_raft;
|
||||
if (m_config.reduce_crossing_wall)
|
||||
m_avoid_crossing_perimeters.init_layer(*m_layer);
|
||||
|
||||
if (this->config().gcode_label_objects) {
|
||||
gcode += std::string("; printing object ") + instance_to_print.print_object.model_object()->name +
|
||||
" id:" + std::to_string(instance_to_print.print_object.get_id()) + " copy " +
|
||||
std::to_string(inst.id) + "\n";
|
||||
}
|
||||
if (m_enable_exclude_object) {
|
||||
if (is_BBL_Printer()) {
|
||||
m_writer.set_object_start_str(
|
||||
std::string("; start printing object, unique label id: ") +
|
||||
std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " +
|
||||
_encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n");
|
||||
} else {
|
||||
const auto gflavor = print.config().gcode_flavor.value;
|
||||
if (gflavor == gcfKlipper) {
|
||||
m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") +
|
||||
get_instance_name(&instance_to_print.print_object, inst.id) + "\n");
|
||||
} else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) {
|
||||
m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_extrusion_quality_estimator.set_current_object(&instance_to_print.print_object);
|
||||
|
||||
const Point &offset = inst.shift;
|
||||
std::pair<const PrintObject*, Point> this_object_copy(&instance_to_print.print_object, offset);
|
||||
if (m_last_obj_copy != this_object_copy)
|
||||
m_avoid_crossing_perimeters.use_external_mp_once();
|
||||
m_last_obj_copy = this_object_copy;
|
||||
this->set_origin(unscale(offset));
|
||||
|
||||
// --- Build emission plan ---
|
||||
// Each entry represents one travel_to_z + extrude pass. Per-object mode produces
|
||||
// exactly 1 entry (all regions, single sub_z); per-volume mode produces N entries
|
||||
// for tagged volumes plus an optional entry for untagged residue.
|
||||
struct SubLayerEmitEntry {
|
||||
double sub_h;
|
||||
double sub_z;
|
||||
std::function<bool(size_t region_idx)> region_filter;
|
||||
bool skip = false;
|
||||
};
|
||||
std::vector<SubLayerEmitEntry> emit_plan;
|
||||
|
||||
auto compute_sub_zh = [&](double r1, double r2, double &out_sub_h, double &out_sub_z) {
|
||||
std::vector<double> sub_heights_local(grp.components_0based.size());
|
||||
for (size_t ci = 0; ci < grp.components_0based.size(); ++ci)
|
||||
sub_heights_local[ci] = (static_cast<int>(ci) == grp.gradient_first_sorted_idx) ? r1 * lh : r2 * lh;
|
||||
double cum = 0.0;
|
||||
for (int ci = 0; ci < sub_idx; ++ci)
|
||||
cum += sub_heights_local[ci];
|
||||
out_sub_h = sub_heights_local[sub_idx];
|
||||
out_sub_z = print_z - lh + cum + out_sub_h;
|
||||
};
|
||||
|
||||
auto gradient_ratios = [](const auto &g) -> std::pair<double, double> {
|
||||
double t = (g.total_layers > 0) ? (2.0 * g.current_idx + 1.0) / (2.0 * g.total_layers) : 0.5;
|
||||
// Custom curve wins over linear range when present; OFF path stays bit-identical.
|
||||
double r1 = g.curve.empty()
|
||||
? (g.gradient_start + (g.gradient_end - g.gradient_start) * t)
|
||||
: sample_gradient_curve(g.curve, t);
|
||||
return {r1, 1.0 - r1};
|
||||
};
|
||||
|
||||
// Orca splits BBS's three role filaments into five; a region belongs to the slot
|
||||
// when any of its roles is assigned to it.
|
||||
auto region_uses_slot = [](const PrintRegionConfig &rcfg, unsigned int slot_1b) {
|
||||
return (unsigned int)rcfg.outer_wall_filament_id.value == slot_1b
|
||||
|| (unsigned int)rcfg.inner_wall_filament_id.value == slot_1b
|
||||
|| (unsigned int)rcfg.sparse_infill_filament_id.value == slot_1b
|
||||
|| (unsigned int)rcfg.internal_solid_filament_id.value == slot_1b
|
||||
|| (unsigned int)rcfg.top_surface_filament_id.value == slot_1b
|
||||
|| (unsigned int)rcfg.bottom_surface_filament_id.value == slot_1b;
|
||||
};
|
||||
|
||||
double obj_sub_z = default_sub_z;
|
||||
|
||||
if (use_per_volume) {
|
||||
const PrintObject *po = &instance_to_print.print_object;
|
||||
const unsigned int slot_1b = grp.mixed_slot_0based + 1;
|
||||
|
||||
// Discover tagged volumes and untagged presence for this instance.
|
||||
std::set<ObjectID> tagged_volumes_present;
|
||||
bool has_untagged_for_slot = false;
|
||||
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
|
||||
for (size_t r = 0; r < island.by_region.size(); ++r) {
|
||||
const auto ®ion = island.by_region[r];
|
||||
if (region.perimeters.empty() && region.infills.empty())
|
||||
continue;
|
||||
const PrintRegion &pr = print.get_print_region(r);
|
||||
if (!region_uses_slot(pr.config(), slot_1b))
|
||||
continue;
|
||||
ObjectID vid = pr.gradient_volume_id();
|
||||
if (vid.valid())
|
||||
tagged_volumes_present.insert(vid);
|
||||
else
|
||||
has_untagged_for_slot = true;
|
||||
}
|
||||
}
|
||||
|
||||
// One entry per tagged volume.
|
||||
for (const ObjectID &target_vid : tagged_volumes_present) {
|
||||
auto vg_it = grp.per_volume_gradient.find({po, target_vid});
|
||||
if (vg_it == grp.per_volume_gradient.end())
|
||||
continue;
|
||||
const auto &vg = vg_it->second;
|
||||
auto [r1, r2] = gradient_ratios(vg);
|
||||
|
||||
bool vol_no_split = false;
|
||||
bool skip_entry = false;
|
||||
const size_t n = grp.components_0based.size();
|
||||
if (n == 2 && vg.current_idx + 1 == vg.total_layers) {
|
||||
const size_t dom_idx = (r1 >= r2) ? 0 : 1;
|
||||
const unsigned int first_sorted_comp = grp.components_0based[grp.gradient_first_sorted_idx];
|
||||
const unsigned int other_comp = grp.components_0based[1 - grp.gradient_first_sorted_idx];
|
||||
const unsigned int dom_0b = (dom_idx == 0) ? first_sorted_comp : other_comp;
|
||||
const unsigned int oth_0b = (dom_idx == 0) ? other_comp : first_sorted_comp;
|
||||
if (dom_0b < oth_0b) {
|
||||
vol_no_split = true;
|
||||
if (extruder_id != dom_0b)
|
||||
skip_entry = true;
|
||||
}
|
||||
}
|
||||
|
||||
double vol_sub_h = default_sub_h;
|
||||
double vol_sub_z = default_sub_z;
|
||||
if (vol_no_split) {
|
||||
vol_sub_h = lh;
|
||||
vol_sub_z = print_z;
|
||||
} else {
|
||||
compute_sub_zh(r1, r2, vol_sub_h, vol_sub_z);
|
||||
}
|
||||
|
||||
emit_plan.push_back({vol_sub_h, vol_sub_z,
|
||||
[target_vid, &print](size_t r) {
|
||||
return print.get_print_region(r).gradient_volume_id() == target_vid;
|
||||
},
|
||||
skip_entry});
|
||||
}
|
||||
|
||||
// Optional entry for untagged regions (modifier / painted / fuzzy_skin).
|
||||
if (has_untagged_for_slot) {
|
||||
double obj_sub_h = default_sub_h;
|
||||
auto og_it = grp.per_object_gradient.find(po);
|
||||
if (og_it != grp.per_object_gradient.end()) {
|
||||
auto [r1, r2] = gradient_ratios(og_it->second);
|
||||
compute_sub_zh(r1, r2, obj_sub_h, obj_sub_z);
|
||||
}
|
||||
emit_plan.push_back({obj_sub_h, obj_sub_z,
|
||||
[&print](size_t r) {
|
||||
return !print.get_print_region(r).gradient_volume_id().valid();
|
||||
},
|
||||
false});
|
||||
}
|
||||
} else {
|
||||
// Legacy per-object path: single entry, no region filter.
|
||||
double legacy_sub_h = default_sub_h;
|
||||
obj_sub_z = default_sub_z;
|
||||
if (grp.is_gradient) {
|
||||
auto og_it = grp.per_object_gradient.find(&instance_to_print.print_object);
|
||||
if (og_it != grp.per_object_gradient.end()) {
|
||||
auto [r1, r2] = gradient_ratios(og_it->second);
|
||||
compute_sub_zh(r1, r2, legacy_sub_h, obj_sub_z);
|
||||
}
|
||||
}
|
||||
emit_plan.push_back({legacy_sub_h, obj_sub_z, nullptr, false});
|
||||
}
|
||||
|
||||
// --- Unified emission loop ---
|
||||
auto plan_has_infill = [](const std::vector<ObjectByExtruder::Island::Region> &by_region) {
|
||||
for (const auto &r : by_region)
|
||||
if (!r.infills.empty())
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
for (auto &entry : emit_plan) {
|
||||
if (entry.skip)
|
||||
continue;
|
||||
m_sub_layer_flow_ratio = entry.sub_h / lh;
|
||||
m_sub_layer_height = entry.sub_h;
|
||||
m_nominal_z = entry.sub_z;
|
||||
// Use the same lazy-Z mechanism as change_layer(): set the flag so travel_to
|
||||
// fires even when m_last_pos coincides with the first extrusion point,
|
||||
// ensuring Z reaches sub_z via the combined XY+Z move.
|
||||
m_need_change_layer_lift_z = true;
|
||||
|
||||
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
|
||||
const auto &src = island.by_region;
|
||||
std::vector<ObjectByExtruder::Island::Region> subset_storage;
|
||||
if (entry.region_filter) {
|
||||
subset_storage.resize(src.size());
|
||||
for (size_t r = 0; r < src.size(); ++r)
|
||||
if (entry.region_filter(r))
|
||||
subset_storage[r] = src[r];
|
||||
}
|
||||
const auto &by_region_specific = entry.region_filter ? subset_storage : src;
|
||||
|
||||
// Orca resolves infill-first per region inside extrude_perimeters()
|
||||
// (unlike BBS, which branches on a single global flag), so mirror the
|
||||
// main instance loop's ordering exactly.
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false);
|
||||
if (!has_wipe_tower && need_insert_timelapse_gcode_for_traditional
|
||||
&& printer_structure == PrinterStructure::psI3
|
||||
&& !has_insert_timelapse_gcode && plan_has_infill(by_region_specific)) {
|
||||
gcode += this->retract(false, false, auto_lift_type, true);
|
||||
gcode += insert_timelapse_gcode();
|
||||
has_insert_timelapse_gcode = true;
|
||||
}
|
||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
|
||||
// ironing
|
||||
gcode += this->extrude_infill(print, by_region_specific, true);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Shared support ---
|
||||
if (instance_to_print.object_by_extruder.support && !instance_to_print.object_by_extruder.support->empty()) {
|
||||
if (use_per_volume) {
|
||||
m_nominal_z = obj_sub_z;
|
||||
m_need_change_layer_lift_z = true;
|
||||
}
|
||||
ExtrusionRole support_role = instance_to_print.object_by_extruder.support_extrusion_role;
|
||||
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, support_role);
|
||||
// Make sure ironing is the last (Orca names this role erIroning, not erSupportIroning).
|
||||
if (support_role == erMixed || support_role == erSupportMaterialInterface)
|
||||
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, erIroning);
|
||||
}
|
||||
|
||||
// --- Shared instance footer (mirrors Orca's main instance loop) ---
|
||||
if (!m_writer.is_object_start_str_empty()) {
|
||||
m_writer.set_object_start_str("");
|
||||
} else if (m_enable_exclude_object) {
|
||||
if (is_BBL_Printer()) {
|
||||
m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") +
|
||||
std::to_string(instance_to_print.label_object_id) + "\n" +
|
||||
"M625\n");
|
||||
} else {
|
||||
const auto gflavor = print.config().gcode_flavor.value;
|
||||
if (gflavor == gcfKlipper) {
|
||||
m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") +
|
||||
get_instance_name(&instance_to_print.print_object, inst.id) + "\n");
|
||||
} else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) {
|
||||
m_writer.set_object_end_str(std::string("M486 S-1\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_sub_layer_flow_ratio = 0.0;
|
||||
m_sub_layer_height = 0.0;
|
||||
}
|
||||
// Flush any pending object end label before leaving the sublayer block, otherwise the
|
||||
// wipe tower's add_object_end_labels may consume it into a local temp string and the
|
||||
// M625 would be lost for BBL printers.
|
||||
if (!layer_tools.mixed_sub_layer_groups.empty()) {
|
||||
m_writer.add_object_end_labels(gcode);
|
||||
m_nominal_z = print_z;
|
||||
m_need_change_layer_lift_z = true;
|
||||
}
|
||||
|
||||
}
|
||||
if (first_layer) {
|
||||
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
|
||||
@@ -7634,6 +7978,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 +8286,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2543,6 +2543,7 @@ void GCodeProcessorResult::reset() {
|
||||
spiral_vase_mode = false;
|
||||
layer_filaments.clear();
|
||||
filament_change_sequence.clear();
|
||||
used_mixed_filaments.clear();
|
||||
nozzle_change_sequence.clear();
|
||||
optimal_assignment.clear();
|
||||
filament_change_count_map.clear();
|
||||
|
||||
@@ -306,6 +306,9 @@ class Print;
|
||||
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
|
||||
std::vector<unsigned int> nozzle_change_sequence;
|
||||
std::vector<unsigned int> filament_change_sequence;
|
||||
// 0-based mixed (virtual) filament slots actually used on this plate.
|
||||
// Recorded before resolve_mixed_filaments expands them to physical components.
|
||||
std::vector<unsigned int> used_mixed_filaments;
|
||||
std::vector<int> optimal_assignment;
|
||||
// first key stores `from` filament, second keys stores the `to` filament
|
||||
std::map<std::pair<int,int>, int > filament_change_count_map;
|
||||
@@ -357,6 +360,7 @@ class Print;
|
||||
printer_extruder_id = other.printer_extruder_id;
|
||||
layer_filaments = other.layer_filaments;
|
||||
filament_change_sequence = other.filament_change_sequence;
|
||||
used_mixed_filaments = other.used_mixed_filaments;
|
||||
nozzle_change_sequence = other.nozzle_change_sequence;
|
||||
optimal_assignment = other.optimal_assignment;
|
||||
filament_change_count_map = other.filament_change_count_map;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "GCode/ToolOrderUtils.hpp"
|
||||
#include "FilamentGroupUtils.hpp"
|
||||
#include "MultiNozzleUtils.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
@@ -22,8 +24,13 @@
|
||||
#endif
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <libslic3r.h>
|
||||
@@ -84,22 +91,28 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
|
||||
}
|
||||
|
||||
// Return a zero based extruder from the region, or extruder_override if overriden.
|
||||
// The region accessors below resolve mixed-color slots to the physical filament chosen for this
|
||||
// layer by resolve_mixed_filaments(), because a virtual slot id is never a real tool. resolve_mixed()
|
||||
// returns its argument unchanged for every filament that is not a mixed slot.
|
||||
unsigned int LayerTools::wall_extruder_id(const PrintRegion ®ion) const
|
||||
{
|
||||
assert(region.config().outer_wall_filament_id.value > 0);
|
||||
return ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1;
|
||||
unsigned int result = ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1;
|
||||
return resolve_mixed(result);
|
||||
}
|
||||
|
||||
unsigned int LayerTools::sparse_infill_filament_id(const PrintRegion ®ion) const
|
||||
{
|
||||
assert(region.config().sparse_infill_filament_id.value > 0);
|
||||
return ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1;
|
||||
unsigned int result = ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1;
|
||||
return resolve_mixed(result);
|
||||
}
|
||||
|
||||
unsigned int LayerTools::internal_solid_filament_id(const PrintRegion ®ion) const
|
||||
{
|
||||
assert(region.config().internal_solid_filament_id.value > 0);
|
||||
return ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
|
||||
unsigned int result = ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
|
||||
return resolve_mixed(result);
|
||||
}
|
||||
|
||||
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
|
||||
@@ -135,7 +148,8 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c
|
||||
} else
|
||||
extruder = this->extruder_override;
|
||||
|
||||
return (extruder == 0) ? 0 : extruder - 1;
|
||||
unsigned int result = (extruder == 0) ? 0 : extruder - 1;
|
||||
return resolve_mixed(result);
|
||||
}
|
||||
|
||||
static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height)
|
||||
@@ -402,7 +416,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
|
||||
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
|
||||
// so we shouldn't reorder first layer in reorder function
|
||||
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
|
||||
this->resolve_mixed_filaments(print.config());
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
this->enforce_mixed_component_order();
|
||||
m_sorted = true;
|
||||
|
||||
double max_layer_height = 0.;
|
||||
@@ -422,6 +438,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
|
||||
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
|
||||
if (this->insert_wipe_tower_extruder()) {
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
// Orca reorders a second time here (BBS has no such path); re-enforce so the
|
||||
// mixed sub-layer component order survives the extra pass.
|
||||
this->enforce_mixed_component_order();
|
||||
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
|
||||
}
|
||||
|
||||
@@ -433,7 +452,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
|
||||
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
|
||||
// so we shouldn't reorder first layer in reorder function
|
||||
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
|
||||
this->resolve_mixed_filaments(object.print()->config());
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
this->enforce_mixed_component_order();
|
||||
m_sorted = true;
|
||||
|
||||
double max_layer_height = calc_max_layer_height(object.print()->config(), object.config().layer_height);
|
||||
@@ -441,6 +462,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
|
||||
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
|
||||
if (this->insert_wipe_tower_extruder()) {
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
// Orca reorders a second time here (BBS has no such path); re-enforce so the
|
||||
// mixed sub-layer component order survives the extra pass.
|
||||
this->enforce_mixed_component_order();
|
||||
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
|
||||
}
|
||||
|
||||
@@ -723,6 +747,38 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
it_per_layer_extruder_override = per_layer_extruder_switches.begin();
|
||||
unsigned int extruder_override = 0;
|
||||
|
||||
// Pre-compute 1-based IDs of mixed filament slots for per-object tracking.
|
||||
// mixed_slots_1based covers ALL mixed slots (needed by calc_slot_lh for
|
||||
// accurate layer height when a slot skips layers). gradient_slots_1based
|
||||
// and per_part_slots_1based are subsets for gradient-specific logic.
|
||||
std::set<unsigned int> mixed_slots_1based;
|
||||
std::set<unsigned int> gradient_slots_1based;
|
||||
std::set<unsigned int> per_part_slots_1based;
|
||||
{
|
||||
const PrintConfig &cfg = object.print()->config();
|
||||
const auto &is_mixed = cfg.filament_is_mixed.values;
|
||||
const auto &grad_flags = cfg.filament_mixed_gradient.values;
|
||||
const auto &per_part_flags = cfg.filament_mixed_gradient_per_part.values;
|
||||
const auto &comp_strs = cfg.filament_mixed_components.values;
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i])
|
||||
continue;
|
||||
auto comps = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
|
||||
if (comps.size() < 2)
|
||||
continue;
|
||||
mixed_slots_1based.insert(static_cast<unsigned int>(i + 1));
|
||||
// Gradient/per-part are only defined for 2-component slots; keep their
|
||||
// tracking limited to them (mirrors the is_gradient guard at resolve time).
|
||||
if (comps.size() != 2)
|
||||
continue;
|
||||
if (i >= grad_flags.size() || !grad_flags[i])
|
||||
continue;
|
||||
gradient_slots_1based.insert(static_cast<unsigned int>(i + 1));
|
||||
if (i < per_part_flags.size() && per_part_flags[i])
|
||||
per_part_slots_1based.insert(static_cast<unsigned int>(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: collect first layer extruders of an object's wall, which will be used by brim generator
|
||||
int layerCount = 0;
|
||||
std::vector<int> firstLayerExtruders;
|
||||
@@ -732,6 +788,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
for (auto layer : object.layers()) {
|
||||
LayerTools &layer_tools = this->tools_for_layer(layer->print_z);
|
||||
|
||||
m_object_all_layer_indices[&object].push_back(
|
||||
static_cast<size_t>(&layer_tools - m_layer_tools.data()));
|
||||
|
||||
// Override extruder with the next
|
||||
for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override)
|
||||
extruder_override = (int)it_per_layer_extruder_override->second;
|
||||
@@ -739,6 +798,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
// Store the current extruder override (set to zero if no overriden), so that layer_tools.wiping_extrusions().is_overridable_and_mark() will use it.
|
||||
layer_tools.extruder_override = extruder_override;
|
||||
|
||||
// Snapshot extruders before this object's regions to track new additions.
|
||||
const size_t ext_snapshot = layer_tools.extruders.size();
|
||||
|
||||
// What extruders are required to print this object layer?
|
||||
for (const LayerRegion *layerm : layer->regions()) {
|
||||
const PrintRegion ®ion = layerm->region();
|
||||
@@ -805,6 +867,54 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill)
|
||||
layer_tools.has_object = true;
|
||||
}
|
||||
|
||||
// Record mixed slot usage for this object at this layer.
|
||||
// All mixed slots are tracked (not just gradient) so that calc_slot_lh
|
||||
// can compute accurate layer heights even when a slot skips layers.
|
||||
if (!mixed_slots_1based.empty()) {
|
||||
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
|
||||
std::set<unsigned int> seen;
|
||||
for (size_t ei = ext_snapshot; ei < layer_tools.extruders.size(); ++ei) {
|
||||
unsigned int ext_1based = layer_tools.extruders[ei];
|
||||
if (mixed_slots_1based.count(ext_1based) && seen.insert(ext_1based).second)
|
||||
m_mixed_object_layers[ext_1based - 1][&object].push_back(layer_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-part gradient: walk LayerRegions and record which (slot, ModelVolume) pairs
|
||||
// contributed to this layer. Only regions tagged by PrintApply.cpp's get_create_region
|
||||
// (i.e. gradient_volume_id().valid()) are considered, so this loop is a strict no-op
|
||||
// unless per_part_gradient is enabled for at least one slot AND the corresponding
|
||||
// ModelObject has >=2 model-part volumes using that slot. The per-object pass above is
|
||||
// unaffected — both run the same layer's data through orthogonal containers.
|
||||
if (!per_part_slots_1based.empty()) {
|
||||
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
|
||||
std::set<std::pair<unsigned int, ObjectID>> vol_seen;
|
||||
for (const LayerRegion *layerm : layer->regions()) {
|
||||
if (layerm->slices.empty())
|
||||
continue;
|
||||
const PrintRegion ®ion = layerm->region();
|
||||
ObjectID vol_id = region.gradient_volume_id();
|
||||
if (! vol_id.valid())
|
||||
continue;
|
||||
const PrintRegionConfig &rcfg = region.config();
|
||||
// Orca splits BBS's three role slots into five; cover them all so a mixed
|
||||
// slot used by any role is tracked.
|
||||
const unsigned int role_slots[5] = {
|
||||
static_cast<unsigned int>(rcfg.outer_wall_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.inner_wall_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.sparse_infill_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.top_surface_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.bottom_surface_filament_id.value),
|
||||
};
|
||||
for (unsigned int ext_1based : role_slots) {
|
||||
if (ext_1based >= 1
|
||||
&& per_part_slots_1based.count(ext_1based)
|
||||
&& vol_seen.insert({ext_1based, vol_id}).second)
|
||||
m_gradient_volume_layers[ext_1based - 1][{&object, vol_id}].push_back(layer_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
layerCount++;
|
||||
}
|
||||
|
||||
@@ -903,7 +1013,7 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
|
||||
|
||||
//FIXME this is a hack to get the ball rolling.
|
||||
for (LayerTools < : m_layer_tools)
|
||||
lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|
||||
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|
||||
|| lt.print_z < object_bottom_z + EPSILON;
|
||||
|
||||
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
|
||||
@@ -944,6 +1054,84 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure wipe tower vertical continuity:
|
||||
//
|
||||
// (1) Any existing LayerTools sandwiched between two has_wipe_tower layers must itself be a
|
||||
// wipe-tower layer. The LayerTools entry already exists, but it has neither object nor
|
||||
// support geometry (has_object == false && has_support == false), so the marking pass
|
||||
// above leaves has_wipe_tower == false. Happens e.g. when one object is fully floating
|
||||
// above another and the support_top_z_distance / support_bottom_z_distance gap leaves an
|
||||
// interior layer with no object and no support (e.g. B top z=20.4, A first layer z=20.8,
|
||||
// the z=20.6 LayerTools entry exists but stays unmarked).
|
||||
//
|
||||
// (2) When two adjacent has_wipe_tower layers are farther apart than max_layer_height and no
|
||||
// LayerTools entry exists between them, insert virtual wipe-tower-only layers to bridge
|
||||
// the gap. Happens with raft: BambuStudio's raft contact layer can be thicker than
|
||||
// max_layer_height (e.g. raft base top z=0.2, raft contact top z=0.5 — gap 0.3 > 0.28),
|
||||
// and there is no LayerTools entry between those two z values.
|
||||
//
|
||||
// wipe_tower_partitions has already been max-propagated downward above, so partition counts
|
||||
// on the filled-in / inserted layers stay consistent.
|
||||
{
|
||||
int first_wt_idx = -1;
|
||||
int last_wt_idx = -1;
|
||||
for (int i = 0; i < (int)m_layer_tools.size(); ++i)
|
||||
if (m_layer_tools[i].has_wipe_tower) {
|
||||
if (first_wt_idx < 0) first_wt_idx = i;
|
||||
last_wt_idx = i;
|
||||
}
|
||||
for (int i = first_wt_idx + 1; i < last_wt_idx; ++i) {
|
||||
LayerTools < = m_layer_tools[i];
|
||||
lt.has_wipe_tower = true;
|
||||
// GCode::process_layer emits wipe-tower G-code inside `for (extruder_id : layer_tools.extruders)`.
|
||||
// An empty extruders vector here would silently skip wipe tower output, leaving the tower
|
||||
// physically floating. Seed from the nearest non-empty neighbor so the loop actually runs.
|
||||
if (lt.extruders.empty()) {
|
||||
unsigned int seed_extruder = 0;
|
||||
bool found_seed = false;
|
||||
for (int j = i - 1; j >= 0; --j)
|
||||
if (!m_layer_tools[j].extruders.empty()) {
|
||||
seed_extruder = m_layer_tools[j].extruders.back();
|
||||
found_seed = true;
|
||||
break;
|
||||
}
|
||||
if (!found_seed)
|
||||
for (int j = i + 1; j < (int)m_layer_tools.size(); ++j)
|
||||
if (!m_layer_tools[j].extruders.empty()) {
|
||||
seed_extruder = m_layer_tools[j].extruders.front();
|
||||
found_seed = true;
|
||||
break;
|
||||
}
|
||||
if (found_seed)
|
||||
lt.extruders.push_back(seed_extruder);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk adjacent has_wipe_tower pairs and split oversized gaps. Re-evaluate the same i
|
||||
// after each insertion so very large gaps get split into multiple layers.
|
||||
for (int i = 0; i + 1 < (int)m_layer_tools.size(); ) {
|
||||
LayerTools < = m_layer_tools[i];
|
||||
LayerTools <_next = m_layer_tools[i + 1];
|
||||
if (!lt.has_wipe_tower || !lt_next.has_wipe_tower) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
coordf_t gap = lt_next.print_z - lt.print_z;
|
||||
if (gap <= max_layer_height + EPSILON) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
LayerTools lt_new(0.5 * (lt.print_z + lt_next.print_z));
|
||||
lt_new.has_wipe_tower = true;
|
||||
if (!lt_next.extruders.empty())
|
||||
lt_new.extruders.push_back(lt_next.extruders.front());
|
||||
else if (!lt.extruders.empty())
|
||||
lt_new.extruders.push_back(lt.extruders.back());
|
||||
lt_new.wipe_tower_partitions = lt_next.wipe_tower_partitions;
|
||||
m_layer_tools.insert(m_layer_tools.begin() + i + 1, lt_new);
|
||||
}
|
||||
}
|
||||
|
||||
// If the model contains empty layers (such as https://github.com/prusa3d/Slic3r/issues/1266), there might be layers
|
||||
// that were not marked as has_wipe_tower, even when they should have been. This produces a crash with soluble supports
|
||||
// and maybe other problems. We will therefore go through layer_tools and detect and fix this.
|
||||
@@ -1945,6 +2133,605 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_
|
||||
return result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult();
|
||||
}
|
||||
|
||||
static double snap_to_simple_fraction(double r, int max_denom = 10)
|
||||
{
|
||||
double best_r = r;
|
||||
double best_err = 1.0;
|
||||
for (int q = 1; q <= max_denom; ++q) {
|
||||
int p = (int)std::round(r * q);
|
||||
if (p < 0) p = 0;
|
||||
if (p > q) p = q;
|
||||
double candidate = (double)p / q;
|
||||
double err = std::abs(candidate - r);
|
||||
if (err < best_err) {
|
||||
best_err = err;
|
||||
best_r = candidate;
|
||||
}
|
||||
}
|
||||
return best_r;
|
||||
}
|
||||
|
||||
void ToolOrdering::resolve_mixed_filaments(const PrintConfig &config)
|
||||
{
|
||||
const auto &is_mixed = config.filament_is_mixed.values;
|
||||
const auto &comp_strs = config.filament_mixed_components.values;
|
||||
const auto &ratio_strs = config.filament_mixed_sublayer_ratios.values;
|
||||
|
||||
// Capture mixed slots that actually appear on layers before they are expanded to
|
||||
// physical components. Assigned-but-unused mixed slots never enter layer_tools.
|
||||
m_used_mixed_filaments.clear();
|
||||
if (has_any_mixed_filament(is_mixed)) {
|
||||
std::set<unsigned int> used;
|
||||
for (const LayerTools < : m_layer_tools)
|
||||
for (unsigned int ext : lt.extruders)
|
||||
if (ext < is_mixed.size() && is_mixed[ext])
|
||||
used.insert(ext);
|
||||
m_used_mixed_filaments.assign(used.begin(), used.end());
|
||||
}
|
||||
|
||||
if (!has_any_mixed_filament(is_mixed))
|
||||
return;
|
||||
|
||||
const bool sublayer_enabled = config.enable_mixed_color_sublayer.value;
|
||||
|
||||
struct SlotInfo {
|
||||
std::vector<unsigned int> components; // 1-based
|
||||
std::vector<double> ratios;
|
||||
std::vector<long long> accum; // deficit accumulator (integer, unit: 1e-6 mm)
|
||||
};
|
||||
std::vector<SlotInfo> slots(is_mixed.size());
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i])
|
||||
continue;
|
||||
slots[i].components = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
|
||||
if (slots[i].components.size() < 2) {
|
||||
slots[i].components.clear();
|
||||
continue;
|
||||
}
|
||||
for (unsigned int cid : slots[i].components) {
|
||||
unsigned int idx0 = cid - 1;
|
||||
if (idx0 >= is_mixed.size() || (idx0 < is_mixed.size() && is_mixed[idx0])) {
|
||||
slots[i].components.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slots[i].components.empty())
|
||||
continue;
|
||||
slots[i].ratios = parse_mixed_ratios(
|
||||
i < ratio_strs.size() ? ratio_strs[i] : "", slots[i].components.size());
|
||||
if (!sublayer_enabled) {
|
||||
for (double &r : slots[i].ratios)
|
||||
r = snap_to_simple_fraction(r);
|
||||
double sum = 0;
|
||||
for (double r : slots[i].ratios) sum += r;
|
||||
if (sum > 0)
|
||||
for (double &r : slots[i].ratios) r /= sum;
|
||||
}
|
||||
slots[i].accum.assign(slots[i].components.size(), 0LL);
|
||||
}
|
||||
|
||||
// Parse gradient settings per slot
|
||||
const auto &gradient_flags = config.filament_mixed_gradient.values;
|
||||
const auto &gradient_range_strs = config.filament_mixed_gradient_range.values;
|
||||
const auto &gradient_curve_strs = config.filament_mixed_gradient_curve.values;
|
||||
struct GradientInfo {
|
||||
double start = 0.10;
|
||||
double end_val = 0.90;
|
||||
GradientCurve curve; // empty -> use linear (start, end_val); non-empty wins
|
||||
};
|
||||
std::vector<bool> is_gradient(is_mixed.size(), false);
|
||||
std::vector<GradientInfo> gradient_info(is_mixed.size());
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i] || slots[i].components.size() != 2)
|
||||
continue;
|
||||
if (i >= gradient_flags.size() || !gradient_flags[i])
|
||||
continue;
|
||||
is_gradient[i] = true;
|
||||
if (i < gradient_range_strs.size() && !gradient_range_strs[i].empty()) {
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
float v0 = 0, v1 = 0;
|
||||
if (std::sscanf(gradient_range_strs[i].c_str(), "%f,%f", &v0, &v1) == 2 &&
|
||||
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
|
||||
gradient_info[i].start = v0;
|
||||
gradient_info[i].end_val = v1;
|
||||
}
|
||||
}
|
||||
if (i < gradient_curve_strs.size() && !gradient_curve_strs[i].empty())
|
||||
gradient_info[i].curve = parse_gradient_curve(gradient_curve_strs[i]);
|
||||
}
|
||||
|
||||
// Pass 1: identify continuous runs for each gradient slot (Per-Run).
|
||||
// A "run" is a maximal sequence of consecutive layers where the slot appears.
|
||||
struct GradientRunInfo {
|
||||
std::vector<size_t> run_lengths;
|
||||
int current_run = -1;
|
||||
size_t current_idx = 0;
|
||||
bool prev_appeared = false;
|
||||
bool last_absent_was_relevant = false;
|
||||
};
|
||||
std::map<unsigned int, GradientRunInfo> gradient_runs;
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i)
|
||||
if (is_gradient[i]) gradient_runs[static_cast<unsigned int>(i)] = {};
|
||||
|
||||
// Build per-slot sets of all layer indices where any slot-owning object has a
|
||||
// layer. Used by gradient run detection (a gap is real only if the slot is
|
||||
// absent at a layer belonging to one of its own objects) and by calc_slot_lh
|
||||
// to keep prev_relevant_z_for_slot current even when a slot skips many layers.
|
||||
std::map<unsigned int, std::set<size_t>> slot_relevant_layers;
|
||||
for (auto &[slot_idx, obj_map] : m_mixed_object_layers) {
|
||||
for (auto &[obj, _] : obj_map) {
|
||||
auto it = m_object_all_layer_indices.find(obj);
|
||||
if (it != m_object_all_layer_indices.end())
|
||||
slot_relevant_layers[slot_idx].insert(it->second.begin(), it->second.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (!gradient_runs.empty()) {
|
||||
for (size_t li = 0; li < m_layer_tools.size(); ++li) {
|
||||
if (li == 0) continue;
|
||||
const auto < = 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 < : m_layer_tools) {
|
||||
size_t layer_idx = static_cast<size_t>(< - m_layer_tools.data());
|
||||
|
||||
// Update gradient run state (skip first layer to match counting).
|
||||
if (layer_idx > 0) {
|
||||
for (auto &[slot, run] : gradient_runs) {
|
||||
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
|
||||
if (here) {
|
||||
if (!run.prev_appeared) {
|
||||
if (run.last_absent_was_relevant || run.current_run < 0) {
|
||||
run.current_run++;
|
||||
run.current_idx = 0;
|
||||
}
|
||||
}
|
||||
run.last_absent_was_relevant = false;
|
||||
} else {
|
||||
auto rel_it = slot_relevant_layers.find(slot);
|
||||
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(layer_idx))
|
||||
run.last_absent_was_relevant = true;
|
||||
}
|
||||
run.prev_appeared = here;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned int> new_extruders;
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
if (ext >= slots.size() || slots[ext].components.empty()) {
|
||||
new_extruders.push_back(ext);
|
||||
continue;
|
||||
}
|
||||
auto &s = slots[ext];
|
||||
|
||||
// Skip sublayer splitting for the first layer to preserve bed adhesion.
|
||||
if (sublayer_enabled && layer_idx > 0) {
|
||||
double lh = calc_slot_lh(ext, lt.print_z);
|
||||
size_t n = s.components.size();
|
||||
|
||||
std::vector<double> sub_heights;
|
||||
bool gradient_last_no_split = false;
|
||||
unsigned int gradient_last_dominant_0b = 0;
|
||||
if (is_gradient[ext] && n == 2) {
|
||||
auto gr_it = gradient_runs.find(ext);
|
||||
if (gr_it != gradient_runs.end() && gr_it->second.current_run >= 0 &&
|
||||
static_cast<size_t>(gr_it->second.current_run) < gr_it->second.run_lengths.size()) {
|
||||
auto &run = gr_it->second;
|
||||
size_t N = run.run_lengths[run.current_run];
|
||||
size_t idx = run.current_idx++;
|
||||
double t = (N > 0) ? (2.0 * idx + 1.0) / (2.0 * N) : 0.5;
|
||||
// Custom curve wins over linear range when present; OFF path stays bit-identical.
|
||||
double r1 = gradient_info[ext].curve.empty()
|
||||
? (gradient_info[ext].start + (gradient_info[ext].end_val - gradient_info[ext].start) * t)
|
||||
: sample_gradient_curve(gradient_info[ext].curve, t);
|
||||
double r2 = 1.0 - r1;
|
||||
sub_heights.push_back(r1 * lh);
|
||||
sub_heights.push_back(r2 * lh);
|
||||
// The sublayer split path sorts components by physical ID ascending;
|
||||
// the higher-ID component ends up on top (visible surface). If the
|
||||
// gradient's dominant component has the lower physical ID, splitting
|
||||
// would put the non-dominant color on the visible top surface. In
|
||||
// that case, skip the split and print this final run-layer as pure
|
||||
// dominant color to preserve the gradient appearance.
|
||||
if (idx == N - 1) {
|
||||
// When r1 == r2 (exactly 50/50), component[0] is treated as dominant.
|
||||
size_t dominant = (r1 >= r2) ? 0 : 1;
|
||||
unsigned int dom_0b = s.components[dominant] - 1;
|
||||
unsigned int oth_0b = s.components[1 - dominant] - 1;
|
||||
if (dom_0b < oth_0b) {
|
||||
gradient_last_no_split = true;
|
||||
gradient_last_dominant_0b = dom_0b;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (double r : s.ratios)
|
||||
sub_heights.push_back(r * lh);
|
||||
}
|
||||
} else {
|
||||
for (double r : s.ratios)
|
||||
sub_heights.push_back(r * lh);
|
||||
}
|
||||
|
||||
// Per-part gradient: when this slot has any qualifying volume, the global
|
||||
// no-split short-circuit must NOT bypass MixedSubLayerGroup creation — each
|
||||
// volume needs its own no-split decision in GCode.cpp (a per-volume "last
|
||||
// run-layer" can occur on a different layer index than the per-object one). We
|
||||
// still keep the per-object short-circuit when per_vol_runs[ext] is empty, which
|
||||
// covers the legacy path bit-identically.
|
||||
bool per_vol_active_for_slot = per_vol_runs.find(ext) != per_vol_runs.end()
|
||||
&& !per_vol_runs[ext].empty();
|
||||
|
||||
if (gradient_last_no_split && !per_vol_active_for_slot) {
|
||||
lt.mixed_filament_resolution[ext] = gradient_last_dominant_0b;
|
||||
new_extruders.push_back(gradient_last_dominant_0b);
|
||||
prev_print_z_for_slot[ext] = lt.print_z;
|
||||
continue;
|
||||
}
|
||||
|
||||
LayerTools::MixedSubLayerGroup grp;
|
||||
grp.mixed_slot_0based = ext;
|
||||
grp.layer_height = lh;
|
||||
grp.is_gradient = is_gradient[ext];
|
||||
for (size_t k = 0; k < s.components.size(); ++k) {
|
||||
unsigned int comp_0based = s.components[k] - 1;
|
||||
grp.components_0based.push_back(comp_0based);
|
||||
}
|
||||
grp.sub_heights = sub_heights;
|
||||
|
||||
// Write gradient metadata (run-aware). Both per_object_gradient and
|
||||
// per_volume_gradient are populated independently from their own run-state
|
||||
// machines; the GCode emitter chooses per-region:
|
||||
// - tagged region (gradient_volume_id valid) -> per_volume_gradient[{obj, vol}]
|
||||
// - untagged region (modifier / painted / etc.) -> per_object_gradient[obj]
|
||||
// Populating both keeps the per-object run state correct even when per-volume
|
||||
// takes over for the same (slot, obj), and lets untagged geometry (which is
|
||||
// never split per-volume) keep its per-object gradient ratios.
|
||||
if (grp.is_gradient) {
|
||||
auto vol_runs_slot_it = per_vol_runs.find(ext);
|
||||
if (vol_runs_slot_it != per_vol_runs.end()) {
|
||||
auto vol_slot_it = m_gradient_volume_layers.find(ext);
|
||||
for (auto &[vkey, st] : vol_runs_slot_it->second) {
|
||||
auto &layer_indices = vol_slot_it->second[vkey];
|
||||
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
|
||||
continue;
|
||||
if (st.current_run < 0 ||
|
||||
st.current_idx >= st.run_lengths[st.current_run]) {
|
||||
st.current_run++;
|
||||
st.current_idx = 0;
|
||||
}
|
||||
size_t run_N = st.run_lengths[st.current_run];
|
||||
size_t run_idx = st.current_idx++;
|
||||
grp.per_volume_gradient[vkey] = {
|
||||
run_N,
|
||||
run_idx,
|
||||
gradient_info[ext].start,
|
||||
gradient_info[ext].end_val,
|
||||
gradient_info[ext].curve,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
auto runs_slot_it = per_obj_runs.find(ext);
|
||||
if (runs_slot_it != per_obj_runs.end()) {
|
||||
auto slot_it = m_mixed_object_layers.find(ext);
|
||||
for (auto &[obj, st] : runs_slot_it->second) {
|
||||
auto &layer_indices = slot_it->second[obj];
|
||||
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
|
||||
continue;
|
||||
if (st.current_run < 0 ||
|
||||
st.current_idx >= st.run_lengths[st.current_run]) {
|
||||
st.current_run++;
|
||||
st.current_idx = 0;
|
||||
}
|
||||
size_t run_N = st.run_lengths[st.current_run];
|
||||
size_t run_idx = st.current_idx++;
|
||||
grp.per_object_gradient[obj] = {
|
||||
run_N,
|
||||
run_idx,
|
||||
gradient_info[ext].start,
|
||||
gradient_info[ext].end_val,
|
||||
gradient_info[ext].curve,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (grp.components_0based.size() > 1) {
|
||||
unsigned int first_comp_0based = s.components[0] - 1;
|
||||
std::vector<size_t> idx(grp.components_0based.size());
|
||||
std::iota(idx.begin(), idx.end(), 0);
|
||||
std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b) {
|
||||
return grp.components_0based[a] < grp.components_0based[b];
|
||||
});
|
||||
std::vector<unsigned int> sorted_comps;
|
||||
std::vector<double> sorted_heights;
|
||||
for (size_t i : idx) {
|
||||
sorted_comps.push_back(grp.components_0based[i]);
|
||||
sorted_heights.push_back(grp.sub_heights[i]);
|
||||
}
|
||||
grp.components_0based = std::move(sorted_comps);
|
||||
grp.sub_heights = std::move(sorted_heights);
|
||||
if (grp.is_gradient) {
|
||||
for (size_t i = 0; i < grp.components_0based.size(); ++i) {
|
||||
if (grp.components_0based[i] == first_comp_0based) {
|
||||
grp.gradient_first_sorted_idx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned int comp : grp.components_0based)
|
||||
new_extruders.push_back(comp);
|
||||
lt.mixed_sub_layer_groups.push_back(std::move(grp));
|
||||
prev_print_z_for_slot[ext] = lt.print_z;
|
||||
} else {
|
||||
// Deficit Round-Robin: pick one component per layer.
|
||||
// Weight by layer height so volume ratios stay accurate
|
||||
// even with adaptive layer heights.
|
||||
double lh = calc_slot_lh(ext, lt.print_z);
|
||||
long long lh_i = std::llround(lh * 1e6);
|
||||
|
||||
// For 2-component gradient on the first layer, use the gradient's
|
||||
// starting ratio instead of the configured mixing ratio so the
|
||||
// selected filament matches the gradient's "from" end.
|
||||
// Only affects the first layer; when sublayer splitting is enabled
|
||||
// (required for gradient), layers 1+ take the sublayer path and
|
||||
// do not touch the DRR accumulator.
|
||||
if (layer_idx == 0 && is_gradient[ext] && s.components.size() == 2) {
|
||||
double r0 = gradient_info[ext].start;
|
||||
s.accum[0] += std::llround(r0 * lh_i);
|
||||
s.accum[1] += std::llround((1.0 - r0) * lh_i);
|
||||
} else {
|
||||
for (size_t k = 0; k < s.ratios.size(); ++k)
|
||||
s.accum[k] += std::llround(s.ratios[k] * lh_i);
|
||||
}
|
||||
size_t sel = 0;
|
||||
for (size_t k = 1; k < s.accum.size(); ++k)
|
||||
if (s.accum[k] > s.accum[sel])
|
||||
sel = k;
|
||||
s.accum[sel] -= lh_i;
|
||||
unsigned int resolved = s.components[sel] - 1;
|
||||
lt.mixed_filament_resolution[ext] = resolved;
|
||||
new_extruders.push_back(resolved);
|
||||
prev_print_z_for_slot[ext] = lt.print_z;
|
||||
}
|
||||
}
|
||||
lt.extruders = new_extruders;
|
||||
sort_remove_duplicates(lt.extruders);
|
||||
|
||||
// Update prev_relevant_z: for each slot that has relevant-layer tracking,
|
||||
// advance if the current layer belongs to a slot-owning object.
|
||||
for (auto &[slot, rel_set] : slot_relevant_layers) {
|
||||
if (rel_set.count(layer_idx))
|
||||
prev_relevant_z_for_slot[slot] = lt.print_z;
|
||||
}
|
||||
|
||||
prev_print_z = lt.print_z;
|
||||
}
|
||||
}
|
||||
|
||||
void ToolOrdering::enforce_mixed_component_order()
|
||||
{
|
||||
for (LayerTools < : m_layer_tools) {
|
||||
if (lt.mixed_sub_layer_groups.empty())
|
||||
continue;
|
||||
|
||||
// Build a set of extruders present in lt.extruders for fast lookup.
|
||||
std::set<unsigned int> ext_set(lt.extruders.begin(), lt.extruders.end());
|
||||
|
||||
// 1. Build DAG from mixed group constraints.
|
||||
// For each group [c0, c1, c2, ...], add edges c0->c1, c1->c2, ...
|
||||
// Only between components that are both present in lt.extruders.
|
||||
// Use an edge set to avoid duplicate edges inflating in-degree.
|
||||
std::map<unsigned int, std::vector<unsigned int>> adj;
|
||||
std::map<unsigned int, int> in_degree;
|
||||
std::set<std::pair<unsigned int, unsigned int>> edge_set;
|
||||
|
||||
for (unsigned int ext : lt.extruders)
|
||||
in_degree[ext] = 0;
|
||||
|
||||
for (const auto &grp : lt.mixed_sub_layer_groups) {
|
||||
for (size_t i = 0; i + 1 < grp.components_0based.size(); ++i) {
|
||||
unsigned int a = grp.components_0based[i];
|
||||
unsigned int b = grp.components_0based[i + 1];
|
||||
if (!ext_set.count(a) || !ext_set.count(b))
|
||||
continue;
|
||||
if (edge_set.insert({a, b}).second) {
|
||||
adj[a].push_back(b);
|
||||
in_degree[b] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Record original position (from flush optimizer) as priority.
|
||||
std::map<unsigned int, size_t> orig_pos;
|
||||
for (size_t i = 0; i < lt.extruders.size(); ++i)
|
||||
orig_pos[lt.extruders[i]] = i;
|
||||
|
||||
// 3. Kahn's topological sort with priority queue (prefer original position).
|
||||
auto cmp = [&orig_pos](unsigned int lhs, unsigned int rhs) {
|
||||
return orig_pos[lhs] > orig_pos[rhs]; // min-heap by orig_pos
|
||||
};
|
||||
std::priority_queue<unsigned int, std::vector<unsigned int>, decltype(cmp)> pq(cmp);
|
||||
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
if (in_degree[ext] == 0)
|
||||
pq.push(ext);
|
||||
}
|
||||
|
||||
std::vector<unsigned int> ordered;
|
||||
ordered.reserve(lt.extruders.size());
|
||||
while (!pq.empty()) {
|
||||
unsigned int ext = pq.top();
|
||||
pq.pop();
|
||||
ordered.push_back(ext);
|
||||
if (auto it = adj.find(ext); it != adj.end()) {
|
||||
for (unsigned int next : it->second) {
|
||||
if (--in_degree[next] == 0)
|
||||
pq.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: if topological sort didn't produce all elements, keep original order.
|
||||
if (ordered.size() != lt.extruders.size())
|
||||
ordered = lt.extruders;
|
||||
|
||||
// 4. Verify: every mixed group's component order is preserved as subsequence.
|
||||
for (const auto &grp : lt.mixed_sub_layer_groups) {
|
||||
size_t prev_pos = 0;
|
||||
bool valid = true;
|
||||
for (unsigned int c : grp.components_0based) {
|
||||
if (!ext_set.count(c))
|
||||
continue;
|
||||
auto it = std::find(ordered.begin() + prev_pos, ordered.end(), c);
|
||||
if (it == ordered.end()) { valid = false; break; }
|
||||
prev_pos = (it - ordered.begin()) + 1;
|
||||
}
|
||||
assert(valid && "enforce_mixed_component_order: mixed group subsequence violated");
|
||||
(void)valid;
|
||||
}
|
||||
|
||||
lt.extruders = ordered;
|
||||
}
|
||||
}
|
||||
|
||||
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
|
||||
{
|
||||
const PrintConfig* print_config = m_print_config_ptr;
|
||||
@@ -1998,6 +2785,17 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
std::vector<unsigned int> used_filaments = collect_sorted_used_filaments(layer_filaments);
|
||||
|
||||
std::vector<std::set<int>>geometric_unprintables = m_print->get_geometric_unprintable_filaments();
|
||||
|
||||
// Unprintable sets are keyed by filament id, but a mixed-color slot is virtual: what actually
|
||||
// reaches the nozzle are its components. Expand the slot to those components so a geometric
|
||||
// restriction is applied to the filaments really being printed. No-op without mixed filaments.
|
||||
{
|
||||
const auto &is_mixed = m_print->config().filament_is_mixed.values;
|
||||
const auto &comp_strs = m_print->config().filament_mixed_components.values;
|
||||
if (has_any_mixed_filament(is_mixed))
|
||||
expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs);
|
||||
}
|
||||
|
||||
std::vector<std::set<int>>physical_unprintables = m_print->get_physical_unprintable_filaments(used_filaments);
|
||||
auto filament_unprintable_volumes = m_print->get_filament_unprintable_flow(used_filaments);
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
|
||||
#include "../libslic3r.h"
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
#include "../FilamentGroup.hpp"
|
||||
#include "../FilamentMixer.hpp"
|
||||
#include "../MultiNozzleUtils.hpp"
|
||||
#include "../ExtrusionEntity.hpp"
|
||||
#include "../ObjectID.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -172,6 +176,65 @@ public:
|
||||
// Custom G-code (color change, extruder switch, pause) to be performed before this layer starts to print.
|
||||
const CustomGCode::Item *custom_gcode = nullptr;
|
||||
|
||||
// 0-based mixed filament slot → 0-based resolved physical filament for this layer.
|
||||
// Populated by ToolOrdering::resolve_mixed_filaments(). Empty when no mixed filaments.
|
||||
std::map<unsigned int, unsigned int> mixed_filament_resolution;
|
||||
|
||||
unsigned int resolve_mixed(unsigned int filament_0based) const {
|
||||
auto it = mixed_filament_resolution.find(filament_0based);
|
||||
return (it != mixed_filament_resolution.end()) ? it->second : filament_0based;
|
||||
}
|
||||
|
||||
struct MixedSubLayerGroup {
|
||||
unsigned int mixed_slot_0based;
|
||||
std::vector<unsigned int> components_0based;
|
||||
std::vector<double> sub_heights; // per-component, sum ≈ layer_height
|
||||
double layer_height = 0.; // the actual lh used to compute sub_heights
|
||||
bool is_gradient = false;
|
||||
int gradient_first_sorted_idx = 0; // index of "first" config component after sorting
|
||||
|
||||
struct ObjectGradient {
|
||||
size_t total_layers;
|
||||
size_t current_idx;
|
||||
double gradient_start;
|
||||
double gradient_end;
|
||||
GradientCurve curve; // empty -> linear fallback (start, end); non-empty wins
|
||||
};
|
||||
std::map<const PrintObject*, ObjectGradient> per_object_gradient;
|
||||
|
||||
// Per-volume gradient: same metadata layout as ObjectGradient but keyed by
|
||||
// (PrintObject*, ModelVolume id). Populated only when filament_mixed_gradient_per_part is
|
||||
// enabled for this slot AND the corresponding ModelObject contains >=2 model-part volumes
|
||||
// using this slot. When non-empty for a given (PrintObject*), GCode emission takes the
|
||||
// per-volume path for tagged regions; untagged regions (modifier/painted/fuzzy_skin) still
|
||||
// use per_object_gradient. Both maps are populated in parallel to keep run states correct.
|
||||
struct VolumeKey {
|
||||
const PrintObject* obj;
|
||||
ObjectID volume_id;
|
||||
bool operator<(const VolumeKey &o) const {
|
||||
if (obj != o.obj) return std::less<const PrintObject*>{}(obj, o.obj);
|
||||
return volume_id < o.volume_id;
|
||||
}
|
||||
bool operator==(const VolumeKey &o) const {
|
||||
return obj == o.obj && volume_id == o.volume_id;
|
||||
}
|
||||
};
|
||||
using VolumeGradient = ObjectGradient;
|
||||
std::map<VolumeKey, VolumeGradient> per_volume_gradient;
|
||||
};
|
||||
std::vector<MixedSubLayerGroup> mixed_sub_layer_groups;
|
||||
|
||||
const MixedSubLayerGroup* mixed_group_by_slot(unsigned int slot_id) const {
|
||||
for (const auto &g : mixed_sub_layer_groups)
|
||||
if (g.mixed_slot_0based == slot_id)
|
||||
return &g;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool is_mixed_slot(unsigned int slot_id) const {
|
||||
return mixed_group_by_slot(slot_id) != nullptr;
|
||||
}
|
||||
|
||||
WipingExtrusions& wiping_extrusions() {
|
||||
m_wiping_extrusions.set_layer_tools_ptr(this);
|
||||
return m_wiping_extrusions;
|
||||
@@ -227,6 +290,9 @@ public:
|
||||
|
||||
// For a multi-material print, the printing extruders are ordered in the order they shall be primed.
|
||||
const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; }
|
||||
// 0-based mixed (virtual) slots that appeared on layers before resolve_mixed_filaments
|
||||
// expanded them to physical components.
|
||||
const std::vector<unsigned int>& used_mixed_filaments() const { return m_used_mixed_filaments; }
|
||||
|
||||
// Find LayerTools with the closest print_z.
|
||||
const LayerTools& tools_for_layer(coordf_t print_z) const;
|
||||
@@ -299,6 +365,8 @@ private:
|
||||
void mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height);
|
||||
void collect_extruder_statistics(bool prime_multi_material);
|
||||
void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer);
|
||||
void resolve_mixed_filaments(const PrintConfig &config);
|
||||
void enforce_mixed_component_order();
|
||||
|
||||
// BBS
|
||||
std::vector<unsigned int> generate_first_layer_tool_order(const Print& print);
|
||||
@@ -311,8 +379,26 @@ private:
|
||||
unsigned int m_last_printing_extruder = (unsigned int)-1;
|
||||
// All extruders, which extrude some material over m_layer_tools.
|
||||
std::vector<unsigned int> m_all_printing_extruders;
|
||||
std::vector<unsigned int> m_used_mixed_filaments;
|
||||
const DynamicPrintConfig* m_print_full_config = nullptr;
|
||||
const PrintConfig* m_print_config_ptr = nullptr;
|
||||
|
||||
// Per-object gradient tracking: slot(0-based) -> PrintObject* -> list of layer indices
|
||||
// where that object uses the slot. Populated by collect_extruders, consumed by resolve_mixed_filaments.
|
||||
std::map<unsigned int, std::map<const PrintObject*, std::vector<size_t>>> m_mixed_object_layers;
|
||||
|
||||
// All layer indices (in m_layer_tools) where each object has any layer.
|
||||
// Used by gradient run detection to distinguish real gaps (object has a layer
|
||||
// that doesn't use the slot) from spurious gaps (another object's layer).
|
||||
std::map<const PrintObject*, std::vector<size_t>> m_object_all_layer_indices;
|
||||
|
||||
// Per-volume gradient tracking: slot(0-based) -> (PrintObject*, ModelVolume id) -> list of
|
||||
// layer indices where the given volume contributes to the slot. Populated by collect_extruders
|
||||
// alongside m_mixed_object_layers when per_part gradient is enabled for the slot AND the
|
||||
// ModelObject has >=2 model-part volumes using the slot. Empty for all other configurations,
|
||||
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
|
||||
// no-ops; downstream emission falls through to the per-object branch).
|
||||
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
|
||||
const PrintObject* m_print_object_ptr = nullptr;
|
||||
Print* m_print;
|
||||
bool m_sorted = false;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -53,7 +53,7 @@ bool is_decimal_separator_point()
|
||||
|
||||
double string_to_double_decimal_point(const std::string_view str, size_t* pos /* = nullptr*/)
|
||||
{
|
||||
double out;
|
||||
double out = 0.;
|
||||
size_t p = fast_float::from_chars(str.data(), str.data() + str.size(), out).ptr - str.data();
|
||||
if (pos)
|
||||
*pos = p;
|
||||
|
||||
+129
-26
@@ -1,6 +1,8 @@
|
||||
#include "Model.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "BuildVolume.hpp"
|
||||
#include "TexturePainting.hpp"
|
||||
#include "Format/AssimpImport.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "Model.hpp"
|
||||
@@ -104,6 +106,7 @@ Model& Model::assign_copy(const Model &rhs)
|
||||
this->mk_version = rhs.mk_version;
|
||||
this->md_name = rhs.md_name;
|
||||
this->md_value = rhs.md_value;
|
||||
this->texture_mesh = rhs.texture_mesh;
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -139,6 +142,7 @@ Model& Model::assign_copy(Model &&rhs)
|
||||
this->mk_version = rhs.mk_version;
|
||||
this->md_name = rhs.md_name;
|
||||
this->md_value = rhs.md_value;
|
||||
this->texture_mesh = std::move(rhs.texture_mesh);
|
||||
this->backup_path = std::move(rhs.backup_path);
|
||||
this->object_backup_id_map = std::move(rhs.object_backup_id_map);
|
||||
this->next_object_backup_id = rhs.next_object_backup_id;
|
||||
@@ -239,6 +243,27 @@ _finished:
|
||||
// BBS: add part plate related logic
|
||||
// BBS: backup & restore
|
||||
// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well.
|
||||
// Build a plain geometry ModelObject from a textured mesh. The texture itself is carried
|
||||
// separately on Model::texture_mesh and consumed by the texture import dialog.
|
||||
static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mesh, const std::string& input_file)
|
||||
{
|
||||
std::string object_name = boost::filesystem::path(input_file).filename().string();
|
||||
|
||||
indexed_triangle_set its;
|
||||
its.vertices.resize(tex_mesh.vertices.size());
|
||||
for (size_t i = 0; i < tex_mesh.vertices.size(); ++i)
|
||||
its.vertices[i] = Vec3f(tex_mesh.vertices[i][0], tex_mesh.vertices[i][1], tex_mesh.vertices[i][2]);
|
||||
its.indices.resize(tex_mesh.indices.size());
|
||||
for (size_t i = 0; i < tex_mesh.indices.size(); ++i)
|
||||
its.indices[i] = Vec3i32(tex_mesh.indices[i][0], tex_mesh.indices[i][1], tex_mesh.indices[i][2]);
|
||||
|
||||
its_merge_vertices(its);
|
||||
its_remove_degenerate_faces(its);
|
||||
its_compactify_vertices(its);
|
||||
|
||||
model.add_object(object_name.c_str(), input_file.c_str(), std::move(TriangleMesh(std::move(its))));
|
||||
}
|
||||
|
||||
Model Model::read_from_file(const std::string& input_file,
|
||||
DynamicPrintConfig* config,
|
||||
ConfigSubstitutionContext* config_substitutions,
|
||||
@@ -281,32 +306,85 @@ Model Model::read_from_file(const std::string&
|
||||
result = load_stl(input_file.c_str(), &model, nullptr, stlFn,256);
|
||||
else if (boost::algorithm::iends_with(input_file, ".obj")) {
|
||||
ObjInfo obj_info;
|
||||
result = load_obj(input_file.c_str(), &model, obj_info, message);
|
||||
if (result){
|
||||
ObjDialogInOut in_out;
|
||||
in_out.model = &model;
|
||||
in_out.lost_material_name = obj_info.lost_material_name;
|
||||
ObjParser::MtlData mtl_data;
|
||||
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
|
||||
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
|
||||
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
|
||||
// instead of the flat per-face colour dialog.
|
||||
auto tex_mesh = std::make_shared<TexturedMesh>();
|
||||
std::string obj_dir = boost::filesystem::path(input_file).parent_path().string();
|
||||
if (obj_to_textured_mesh(obj_info,
|
||||
model.objects.back()->volumes[0]->mesh().its,
|
||||
mtl_data, obj_dir, *tex_mesh)) {
|
||||
model.texture_mesh = tex_mesh;
|
||||
}
|
||||
}
|
||||
else if (result && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
|
||||
// Vertex-colour and MTL face-colour OBJs also go through the texture-to-color
|
||||
// importer (as precomputed per-face colors) instead of the flat
|
||||
// per-face colour dialog, matching the uv_png branch above.
|
||||
auto build_tex_mesh_geometry = [&]() {
|
||||
auto tex_mesh = std::make_shared<TexturedMesh>();
|
||||
const auto& its = model.objects.back()->volumes[0]->mesh().its;
|
||||
tex_mesh->vertices.resize(its.vertices.size());
|
||||
for (size_t i = 0; i < its.vertices.size(); ++i)
|
||||
tex_mesh->vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
|
||||
tex_mesh->indices.resize(its.indices.size());
|
||||
for (size_t i = 0; i < its.indices.size(); ++i)
|
||||
tex_mesh->indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
|
||||
return tex_mesh;
|
||||
};
|
||||
if (obj_info.vertex_colors.size() > 0) {
|
||||
if (objFn) { // 1.result is ok and pop up a dialog
|
||||
in_out.input_colors = std::move(obj_info.vertex_colors);
|
||||
in_out.is_single_color = false;
|
||||
in_out.deal_vertex_color = true;
|
||||
objFn(in_out);
|
||||
auto tex_mesh = build_tex_mesh_geometry();
|
||||
const auto& its = model.objects.back()->volumes[0]->mesh().its;
|
||||
tex_mesh->precomputed_face_colors.resize(its.indices.size());
|
||||
for (size_t i = 0; i < its.indices.size(); ++i) {
|
||||
const auto& f = its.indices[i];
|
||||
auto avg = [&](int ch) -> std::size_t {
|
||||
float v = (obj_info.vertex_colors[f[0]][ch]
|
||||
+ obj_info.vertex_colors[f[1]][ch]
|
||||
+ obj_info.vertex_colors[f[2]][ch]) / 3.0f * 255.0f;
|
||||
return (std::size_t) std::clamp(v, 0.0f, 255.0f);
|
||||
};
|
||||
tex_mesh->precomputed_face_colors[i] = {avg(0), avg(1), avg(2)};
|
||||
}
|
||||
} else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { // mtl file
|
||||
if (objFn) { // 1.result is ok and pop up a dialog
|
||||
in_out.input_colors = std::move(obj_info.face_colors);
|
||||
in_out.is_single_color = obj_info.is_single_mtl;
|
||||
in_out.deal_vertex_color = false;
|
||||
objFn(in_out);
|
||||
tex_mesh->precomputed_vertex_colors = obj_info.vertex_colors;
|
||||
model.texture_mesh = tex_mesh;
|
||||
} else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) {
|
||||
auto tex_mesh = build_tex_mesh_geometry();
|
||||
const size_t nf = tex_mesh->indices.size();
|
||||
tex_mesh->precomputed_face_colors.resize(nf);
|
||||
for (size_t i = 0; i < nf; ++i) {
|
||||
if (i < obj_info.face_colors.size()) {
|
||||
const auto& c = obj_info.face_colors[i];
|
||||
tex_mesh->precomputed_face_colors[i] = {
|
||||
(std::size_t) std::clamp(c[0] * 255.0f, 0.0f, 255.0f),
|
||||
(std::size_t) std::clamp(c[1] * 255.0f, 0.0f, 255.0f),
|
||||
(std::size_t) std::clamp(c[2] * 255.0f, 0.0f, 255.0f)
|
||||
};
|
||||
} else {
|
||||
tex_mesh->precomputed_face_colors[i] = {128, 128, 128};
|
||||
}
|
||||
}
|
||||
} /*else if (obj_info.has_uv_png && obj_info.uvs.size() > 0) {
|
||||
boost::filesystem::path full_path(input_file);
|
||||
std::string obj_directory = full_path.parent_path().string();
|
||||
obj_info.obj_dircetory = obj_directory;
|
||||
result = false;
|
||||
message = _L("Importing obj with png function is developing.");
|
||||
}*/
|
||||
model.texture_mesh = tex_mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (boost::algorithm::iends_with(input_file, ".glb") ||
|
||||
boost::algorithm::iends_with(input_file, ".gltf") ||
|
||||
boost::algorithm::iends_with(input_file, ".fbx")) {
|
||||
// These formats can carry material/texture data, so they go through the textured
|
||||
// import path: the geometry becomes a normal object and the texture is handed to the
|
||||
// texture-to-color dialog via Model::texture_mesh.
|
||||
auto tex_mesh = std::make_shared<TexturedMesh>();
|
||||
result = load_assimp_textured_model(input_file, *tex_mesh, &message);
|
||||
if (result) {
|
||||
model.texture_mesh = tex_mesh;
|
||||
add_textured_mesh_to_model(model, *tex_mesh, input_file);
|
||||
} else if (!message.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Assimp: failed to load model: " << message
|
||||
<< ", path=" << input_file;
|
||||
message = _L("The file format is incompatible and cannot be parsed.");
|
||||
}
|
||||
}
|
||||
else if (boost::algorithm::iends_with(input_file, ".svg"))
|
||||
@@ -578,6 +656,7 @@ void Model::clear_objects()
|
||||
this->objects.clear();
|
||||
object_backup_id_map.clear();
|
||||
next_object_backup_id = 1;
|
||||
texture_mesh.reset();
|
||||
}
|
||||
|
||||
// BBS: backup, reuse objects
|
||||
@@ -2576,7 +2655,8 @@ void ModelVolume::update_extruder_count(size_t extruder_count)
|
||||
}
|
||||
}
|
||||
|
||||
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id)
|
||||
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id,
|
||||
const std::vector<unsigned char> &filament_is_mixed)
|
||||
{
|
||||
std::vector<int> used_extruders = get_extruders();
|
||||
for (int extruder_id : used_extruders) {
|
||||
@@ -2587,8 +2667,22 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou
|
||||
}
|
||||
// Same stale-assignment cleanup as update_extruder_count, for the filament-delete path.
|
||||
// Ported from BambuStudio (STUDIO-15763).
|
||||
if (extruder_id() > extruder_count) {
|
||||
this->config.erase("extruder");
|
||||
size_t eid = extruder_id();
|
||||
// Judge out-of-range against the post-remap id, mirroring update_filament_values_for_items_when_delete_filament.
|
||||
// Using the pre-remap eid would wrongly erase a high extruder that should remap (e.g. 5 -> 4 after
|
||||
// deleting filament 1); update_filament_values_for_items_when_delete_filament would then skip it
|
||||
// (!has("extruder")) and the volume would fall back to the object default color.
|
||||
size_t remapped = eid;
|
||||
if (eid == filament_id)
|
||||
remapped = (replace_filament_id > 0) ? (size_t)replace_filament_id : 1;
|
||||
else if (eid > filament_id)
|
||||
remapped = eid - 1;
|
||||
if (remapped > extruder_count) {
|
||||
// filament_is_mixed is the pre-delete snapshot; index it with the ORIGINAL eid (1-based),
|
||||
// not remapped, so we check whether this volume's current slot is a mixed slot.
|
||||
bool is_mixed = !filament_is_mixed.empty() && eid >= 1 && (eid - 1) < filament_is_mixed.size() && filament_is_mixed[eid - 1];
|
||||
if (!is_mixed)
|
||||
this->config.erase("extruder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3495,6 +3589,15 @@ void FacetsAnnotation::get_facets(const ModelVolume& mv, std::vector<indexed_tri
|
||||
selector.get_facets(facets_per_type);
|
||||
}
|
||||
|
||||
void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta)
|
||||
{
|
||||
if (empty()) return;
|
||||
TriangleSelector selector(mv.mesh());
|
||||
selector.deserialize(m_data, false);
|
||||
selector.shift_states_above(threshold, delta);
|
||||
this->set(selector);
|
||||
}
|
||||
|
||||
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
|
||||
EnforcerBlockerType max_type,
|
||||
EnforcerBlockerType to_delete_filament,
|
||||
|
||||
+11
-1
@@ -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;
|
||||
|
||||
@@ -1185,6 +1185,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",
|
||||
|
||||
+2423
-1927
File diff suppressed because it is too large
Load Diff
@@ -517,6 +517,12 @@ public:
|
||||
// Read out the number of extruders from an active printer preset,
|
||||
// update size and content of filament_presets.
|
||||
void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1));
|
||||
// Mixed-color filament slots: virtual slots realized from 2-3 physical filaments.
|
||||
bool is_mixed_filament(size_t idx) const;
|
||||
std::vector<size_t> physical_filament_config_indices() const;
|
||||
// How many slots are mixed. They sit at the tail of the filament list and have no nozzle of
|
||||
// their own, so any resize driven by the printer's extruder count has to add this on top.
|
||||
size_t num_mixed_filaments() const;
|
||||
|
||||
void on_extruders_count_changed(int extruder_count);
|
||||
|
||||
|
||||
+114
-30
@@ -5,6 +5,7 @@
|
||||
#include "Brim.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Extruder.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "Flow.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "I18N.hpp"
|
||||
@@ -565,7 +566,7 @@ std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const
|
||||
|
||||
// If a wipe tower filament is explicitly set, ensure it participates in tool ordering.
|
||||
if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) {
|
||||
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament < int(config().nozzle_diameter.size()));
|
||||
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size()));
|
||||
extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based
|
||||
}
|
||||
|
||||
@@ -1327,6 +1328,19 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
if (extruders.empty())
|
||||
return { L("No extrusions under current settings.") };
|
||||
|
||||
// Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on;
|
||||
// without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and
|
||||
// the gradient is dropped silently. extruders() already covers painting, height ranges,
|
||||
// per-feature filament ids and supports, and still lists mixed slots under their own id here.
|
||||
if (!m_config.enable_mixed_color_sublayer.value) {
|
||||
const auto &is_mixed = m_config.filament_is_mixed.values;
|
||||
const auto &gradient = m_config.filament_mixed_gradient.values;
|
||||
if (std::any_of(extruders.begin(), extruders.end(), [&](unsigned int e) {
|
||||
return e < is_mixed.size() && is_mixed[e] && e < gradient.size() && gradient[e]; }))
|
||||
warn(L("A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed."),
|
||||
"enable_mixed_color_sublayer");
|
||||
}
|
||||
|
||||
if (nozzles < 2 && extruders.size() > 1) {
|
||||
auto ret = check_multi_filament_valid(*this);
|
||||
if (!ret.string.empty())
|
||||
@@ -1388,6 +1402,13 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
// #4043
|
||||
if (total_copies_count > 1 && m_config.print_sequence != PrintSequence::ByObject)
|
||||
return {L("Please select \"By object\" print sequence to print multiple objects in spiral vase mode."), nullptr, "spiral_mode"};
|
||||
// A mixed (virtual) filament always resolves to multiple physical components, which
|
||||
// spiral vase cannot print.
|
||||
const auto &is_mixed = m_config.filament_is_mixed.values;
|
||||
for (const PrintObject *object : m_objects)
|
||||
for (unsigned int ext : object->object_extruders())
|
||||
if (ext < is_mixed.size() && is_mixed[ext])
|
||||
return {L("Spiral (vase) mode does not work when an object contains more than one material."), nullptr, "spiral_mode"};
|
||||
assert(m_objects.size() == 1);
|
||||
const auto all_regions = m_objects.front()->all_regions();
|
||||
if (all_regions.size() > 1) {
|
||||
@@ -1464,6 +1485,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
|
||||
if (this->has_wipe_tower() && ! m_objects.empty()) {
|
||||
// Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after
|
||||
// resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here
|
||||
// would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides
|
||||
// mixed slots from the option; this guards loaded projects and the CLI.
|
||||
if (m_config.wipe_tower_filament > 0) {
|
||||
const auto &is_mixed = m_config.filament_is_mixed.values;
|
||||
const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1);
|
||||
if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx])
|
||||
return { L("The wipe tower filament cannot be a mixed filament."), nullptr, "wipe_tower_filament" };
|
||||
}
|
||||
|
||||
// Make sure all extruders use same diameter filament and have the same nozzle diameter
|
||||
// EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments
|
||||
double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front());
|
||||
@@ -2585,18 +2617,31 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
std::vector<const PrintInstance*>::const_iterator print_object_instance_sequential_active;
|
||||
std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> layers_to_print = GCode::collect_layers_to_print(*this);
|
||||
std::vector<unsigned int> printExtruders;
|
||||
// Per-object first-layer mixed-slot resolutions for the by-object remap below
|
||||
// (BBS reads them from m_sequential_print_data->object_tool_ordering_map).
|
||||
std::map<ObjectID, std::map<unsigned int, unsigned int>> seq_mixed_resolution;
|
||||
// Cleared on every process so a print-sequence or selector-mode change can never leave
|
||||
// stale object pointers behind; repopulated below only by the sequential selector path.
|
||||
m_sequential_dynamic_orderings.clear();
|
||||
if (this->config().print_sequence == PrintSequence::ByObject) {
|
||||
// Order object instances for sequential print.
|
||||
print_object_instances_ordering = sort_object_instances_by_model_order(*this);
|
||||
// A mixed slot is virtual; only its components reach a nozzle. These per-object orderings
|
||||
// are unsorted (no resolve_mixed_filaments), so expand the slots here for the grouping, the
|
||||
// unprintable sets and the slice-used lists. Because the expansion happens here rather than
|
||||
// on the sorted orderings, the first-layer used set lists every component of a mixed slot,
|
||||
// not just the one layer 0 resolves to. No-op without mixed filaments.
|
||||
const auto &is_mixed = m_config.filament_is_mixed.values;
|
||||
const auto &comp_strs = m_config.filament_mixed_components.values;
|
||||
const bool has_mixed = has_any_mixed_filament(is_mixed);
|
||||
std::vector<unsigned int> first_layer_used_filaments;
|
||||
std::vector<std::vector<unsigned int>> all_filaments;
|
||||
for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
|
||||
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
|
||||
for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) {
|
||||
auto& layer_filament = tool_ordering.layer_tools()[idx].extruders;
|
||||
auto layer_filament = tool_ordering.layer_tools()[idx].extruders;
|
||||
if (has_mixed)
|
||||
layer_filament = expand_mixed_filaments(layer_filament, is_mixed, comp_strs);
|
||||
all_filaments.emplace_back(layer_filament);
|
||||
if (idx == 0)
|
||||
first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end());
|
||||
@@ -2608,6 +2653,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
|
||||
auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments);
|
||||
auto geometric_unprintables = this->get_geometric_unprintable_filaments();
|
||||
if (has_mixed)
|
||||
expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs);
|
||||
auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments);
|
||||
// Selector (per-layer regroup) prints skip the static grouping: their print-wide result
|
||||
// is stitched from the per-object plans after the ordering loop below.
|
||||
@@ -2659,6 +2706,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
std::vector<std::vector<int>> nozzle_map_per_layer;
|
||||
std::vector<std::vector<unsigned int>> stitched_layer_filaments;
|
||||
print_object_instance_sequential_active = print_object_instances_ordering.begin();
|
||||
std::vector<unsigned int> used_mixed_filaments;
|
||||
for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
|
||||
const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object;
|
||||
if (dynamic_reorder) {
|
||||
@@ -2687,11 +2735,18 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
} else {
|
||||
tool_ordering = ToolOrdering(*print_object, initial_extruder_id);
|
||||
tool_ordering.sort_and_build_data(*print_object, initial_extruder_id);
|
||||
if (!tool_ordering.layer_tools().empty())
|
||||
seq_mixed_resolution[print_object->id()] = tool_ordering.layer_tools().front().mixed_filament_resolution;
|
||||
}
|
||||
// Only sorted orderings have run resolve_mixed_filaments, so only they know which
|
||||
// mixed slots actually print.
|
||||
append(used_mixed_filaments, tool_ordering.used_mixed_filaments());
|
||||
if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast<unsigned int>(-1)) {
|
||||
append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders);
|
||||
}
|
||||
}
|
||||
sort_remove_duplicates(used_mixed_filaments);
|
||||
this->set_slice_used_mixed_filaments(used_mixed_filaments);
|
||||
if (dynamic_reorder && m_objects.size() > 1) {
|
||||
// Stitch the per-object plans into one print-wide selector result. A single-object
|
||||
// sequential print publishes (and writes back) from its own ordering instead: the
|
||||
@@ -2712,6 +2767,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
first_layer_used_filaments = tool_ordering.layer_tools().front().extruders;
|
||||
|
||||
this->set_slice_used_filaments(first_layer_used_filaments, tool_ordering.all_extruders());
|
||||
this->set_slice_used_mixed_filaments(tool_ordering.used_mixed_filaments());
|
||||
has_wipe_tower = this->has_wipe_tower() && tool_ordering.has_wipe_tower();
|
||||
initial_extruder_id = tool_ordering.first_extruder();
|
||||
print_object_instances_ordering = chain_print_object_instances(*this);
|
||||
@@ -2719,6 +2775,28 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
}
|
||||
|
||||
auto objectExtruderMap = getObjectExtruderMap(*this);
|
||||
// Resolve mixed filament virtual slots to physical components so brim
|
||||
// extruder matching works correctly (mixed slot IDs are not present
|
||||
// in printExtruders after ToolOrdering::resolve_mixed_filaments).
|
||||
{
|
||||
const LayerTools *first_lt = nullptr;
|
||||
if (m_config.print_sequence != PrintSequence::ByObject && !tool_ordering.layer_tools().empty())
|
||||
first_lt = &tool_ordering.layer_tools().front();
|
||||
for (auto &[obj_id, ext_1based] : objectExtruderMap) {
|
||||
if (ext_1based == 0)
|
||||
continue;
|
||||
const std::map<unsigned int, unsigned int> *resolution = nullptr;
|
||||
if (first_lt)
|
||||
resolution = &first_lt->mixed_filament_resolution;
|
||||
else if (auto obj_it = seq_mixed_resolution.find(obj_id); obj_it != seq_mixed_resolution.end())
|
||||
resolution = &obj_it->second;
|
||||
if (resolution) {
|
||||
auto it = resolution->find(ext_1based - 1);
|
||||
if (it != resolution->end())
|
||||
ext_1based = it->second + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::pair<ObjectID, unsigned int>> objPrintVec;
|
||||
for (const PrintInstance* instance : print_object_instances_ordering) {
|
||||
const ObjectID& print_object_ID = instance->print_object->id();
|
||||
@@ -3776,6 +3854,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;
|
||||
}
|
||||
|
||||
@@ -3999,38 +4085,36 @@ void Print::_make_wipe_tower()
|
||||
return;
|
||||
|
||||
// Check whether there are any layers in m_tool_ordering, which are marked with has_wipe_tower,
|
||||
// they print neither object, nor support. These layers are above the raft and below the object, and they
|
||||
// shall be added to the support layers to be printed.
|
||||
// see https://github.com/prusa3d/PrusaSlicer/issues/607
|
||||
// they print neither object, nor support. Each such layer needs a virtual support layer
|
||||
// counterpart in m_objects.front() so that GCode::collect_layers_to_print picks it up and the
|
||||
// wipe tower G-code is actually emitted for that z. Such layers appear in two scenarios:
|
||||
// - above the raft, between raft top and the first real object layer
|
||||
// (see https://github.com/prusa3d/PrusaSlicer/issues/607);
|
||||
// - between two real wipe-tower layers, when one object is fully floating above another and
|
||||
// the support_top_z_distance / support_bottom_z_distance gap leaves interior z values with
|
||||
// neither object nor support (continuity fill in ToolOrdering::fill_wipe_tower_partitions).
|
||||
// The previous implementation only handled the first contiguous run starting at the first
|
||||
// virtual layer, which made the second scenario silently produce empty wipe-tower layers.
|
||||
{
|
||||
size_t idx_begin = size_t(-1);
|
||||
size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
|
||||
// Find the first wipe tower layer, which does not have a counterpart in an object or a support layer.
|
||||
auto &support_layers = m_objects.front()->support_layers();
|
||||
auto it_layer = support_layers.begin();
|
||||
const size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
|
||||
for (size_t i = 0; i < idx_end; ++ i) {
|
||||
const LayerTools < = m_wipe_tower_data.tool_ordering.layer_tools()[i];
|
||||
if (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support) {
|
||||
idx_begin = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx_begin != size_t(-1)) {
|
||||
// Find the position in m_objects.first()->support_layers to insert these new support layers.
|
||||
double wipe_tower_new_layer_print_z_first = m_wipe_tower_data.tool_ordering.layer_tools()[idx_begin].print_z;
|
||||
auto it_layer = m_objects.front()->support_layers().begin();
|
||||
auto it_end = m_objects.front()->support_layers().end();
|
||||
for (; it_layer != it_end && (*it_layer)->print_z - EPSILON < wipe_tower_new_layer_print_z_first; ++ it_layer);
|
||||
// Find the stopper of the sequence of wipe tower layers, which do not have a counterpart in an object or a support layer.
|
||||
for (size_t i = idx_begin; i < idx_end; ++ i) {
|
||||
LayerTools < = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
|
||||
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
|
||||
break;
|
||||
lt.has_support = true;
|
||||
// Insert the new support layer.
|
||||
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
|
||||
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
|
||||
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
|
||||
LayerTools < = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
|
||||
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
|
||||
continue;
|
||||
while (it_layer != support_layers.end() && (*it_layer)->print_z + EPSILON < lt.print_z)
|
||||
++ it_layer;
|
||||
if (it_layer != support_layers.end() && std::abs((*it_layer)->print_z - lt.print_z) < EPSILON) {
|
||||
lt.has_support = true;
|
||||
++ it_layer;
|
||||
continue;
|
||||
}
|
||||
lt.has_support = true;
|
||||
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
|
||||
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
|
||||
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
|
||||
++ it_layer;
|
||||
}
|
||||
}
|
||||
this->throw_if_canceled();
|
||||
|
||||
+23
-4
@@ -117,9 +117,9 @@ class PrintRegion
|
||||
public:
|
||||
PrintRegion() = default;
|
||||
PrintRegion(const PrintRegionConfig &config);
|
||||
PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {}
|
||||
PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {}
|
||||
PrintRegion(PrintRegionConfig &&config);
|
||||
PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {}
|
||||
PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {}
|
||||
~PrintRegion() = default;
|
||||
|
||||
// Methods NOT modifying the PrintRegion's state:
|
||||
@@ -129,6 +129,10 @@ public:
|
||||
// Identifier of this PrintRegion in the list of Print::m_print_regions.
|
||||
int print_region_id() const throw() { return m_print_region_id; }
|
||||
int print_object_region_id() const throw() { return m_print_object_region_id; }
|
||||
// Volume identity used to differentiate same-config regions when per-part gradient is enabled.
|
||||
// Default-constructed (invalid) means this region is not tied to a specific volume — preserves
|
||||
// existing behavior for all paths not using per_part_gradient.
|
||||
ObjectID gradient_volume_id() const throw() { return m_gradient_volume_id; }
|
||||
// 1-based extruder identifier for this region and role.
|
||||
unsigned int extruder(FlowRole role) const;
|
||||
Flow flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer = false) const;
|
||||
@@ -158,6 +162,10 @@ private:
|
||||
int m_print_region_id { -1 };
|
||||
int m_print_object_region_id { -1 };
|
||||
int m_ref_cnt { 0 };
|
||||
// Per-part gradient: when non-invalid, this region belongs exclusively to one ModelVolume,
|
||||
// letting same-color volumes within a combined ModelObject be tracked separately for gradient
|
||||
// emission. Default invalid -> region keying behaves exactly as before.
|
||||
ObjectID m_gradient_volume_id;
|
||||
};
|
||||
|
||||
inline bool operator==(const PrintRegion &lhs, const PrintRegion &rhs) { return lhs.config_hash() == rhs.config_hash() && lhs.config() == rhs.config(); }
|
||||
@@ -306,6 +314,11 @@ public:
|
||||
Transform3d trafo_bboxes;
|
||||
std::vector<ObjectID> cached_volume_ids;
|
||||
|
||||
// Per-part gradient: the slot_per_part_enabled bit vector that produced these regions.
|
||||
// Print::apply compares it against the current one to detect a change that PrintRegionConfig
|
||||
// alone would not reveal, and regenerates the regions when it differs.
|
||||
std::vector<bool> last_slot_per_part_enabled;
|
||||
|
||||
void ref_cnt_inc() { ++ m_ref_cnt; }
|
||||
void ref_cnt_dec() { if (-- m_ref_cnt == 0) delete this; }
|
||||
void clear() {
|
||||
@@ -930,8 +943,8 @@ public:
|
||||
// If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r).
|
||||
std::string export_gcode(const std::string& path_template, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
|
||||
//return 0 means successful
|
||||
int export_cached_data(const std::string& dir_path, bool with_space=false);
|
||||
int load_cached_data(const std::string& directory);
|
||||
int export_cached_data(const std::string& dir_path, bool with_space=false) override;
|
||||
int load_cached_data(const std::string& directory) override;
|
||||
|
||||
// methods for handling state
|
||||
bool is_step_done(PrintStep step) const { return Inherited::is_step_done(step); }
|
||||
@@ -1075,6 +1088,10 @@ public:
|
||||
m_slice_used_filaments = used_filaments;
|
||||
}
|
||||
std::vector<unsigned int> get_slice_used_filaments(bool first_layer) const { return first_layer ? m_slice_used_filaments_first_layer : m_slice_used_filaments;}
|
||||
void set_slice_used_mixed_filaments(const std::vector<unsigned int> &used_mixed_filaments) {
|
||||
m_slice_used_mixed_filaments = used_mixed_filaments;
|
||||
}
|
||||
const std::vector<unsigned int>& get_slice_used_mixed_filaments() const { return m_slice_used_mixed_filaments; }
|
||||
|
||||
/**
|
||||
* @brief Determines the unprintable filaments for each extruder based on its physical attributes
|
||||
@@ -1342,6 +1359,8 @@ private:
|
||||
|
||||
std::vector<unsigned int> m_slice_used_filaments;
|
||||
std::vector<unsigned int> m_slice_used_filaments_first_layer;
|
||||
// 0-based mixed (virtual) filament slots actually used on this plate.
|
||||
std::vector<unsigned int> m_slice_used_mixed_filaments;
|
||||
|
||||
//BBS: plate's origin
|
||||
Vec3d m_origin {0, 0, 0};
|
||||
|
||||
+124
-10
@@ -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 = [®ion_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 = [®ion_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.
|
||||
@@ -1828,7 +1931,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
for (const ModelVolume *volume : volumes) {
|
||||
const std::vector<bool> &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states;
|
||||
|
||||
assert(volume_used_facet_states.size() == used_facet_states.size());
|
||||
// Paint data saved before the painted state range was extended deserializes a
|
||||
// shorter used_states vector, so merge over the common prefix.
|
||||
for (size_t state_idx = 0; state_idx < std::min(volume_used_facet_states.size(), used_facet_states.size()); ++state_idx)
|
||||
used_facet_states[state_idx] |= volume_used_facet_states[state_idx];
|
||||
}
|
||||
@@ -1862,6 +1966,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 +1997,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) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PrintConfigConstants.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
@@ -3263,6 +3264,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 +7459,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.");
|
||||
@@ -9605,7 +9670,15 @@ t_config_option_keys DynamicPrintConfig::normalize_fdm_2(int num_objects, int us
|
||||
ConfigOptionBool *enable_wrapping_opt = this->option<ConfigOptionBool>("enable_wrapping_detection");
|
||||
bool enable_wrapping = enable_wrapping_opt != nullptr && enable_wrapping_opt->value;
|
||||
|
||||
if (!is_smooth_timelapse && !enable_wrapping && (used_filaments == 1 || (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) {
|
||||
bool has_mixed_filament = false;
|
||||
{
|
||||
auto *mixed_opt = this->option<ConfigOptionBools>("filament_is_mixed");
|
||||
if (mixed_opt)
|
||||
has_mixed_filament = has_any_mixed_filament(mixed_opt->values);
|
||||
}
|
||||
if (!is_smooth_timelapse && !enable_wrapping
|
||||
&& ( (used_filaments == 1 && !has_mixed_filament)
|
||||
|| (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) {
|
||||
if (ept_opt->value) {
|
||||
ept_opt->value = false;
|
||||
changed_keys.push_back("enable_prime_tower");
|
||||
@@ -11753,6 +11826,23 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
}
|
||||
}
|
||||
|
||||
// Mixed-color (混色) parameter validation.
|
||||
{
|
||||
const auto &is_mixed = cfg.filament_is_mixed.values;
|
||||
const auto &comp_strs = cfg.filament_mixed_components.values;
|
||||
const auto &ratio_strs = cfg.filament_mixed_sublayer_ratios.values;
|
||||
const auto &gradient_flags = cfg.filament_mixed_gradient.values;
|
||||
const auto &range_strs = cfg.filament_mixed_gradient_range.values;
|
||||
const auto &curve_strs = cfg.filament_mixed_gradient_curve.values;
|
||||
|
||||
std::map<std::string, std::string> mixed_errors = validate_mixed_filament_params(
|
||||
is_mixed, comp_strs, ratio_strs, gradient_flags,
|
||||
range_strs, curve_strs);
|
||||
for (const auto &kv : mixed_errors)
|
||||
if (error_message.find(kv.first) == error_message.end())
|
||||
error_message.emplace(kv.first, kv.second);
|
||||
}
|
||||
|
||||
// The configuration is valid.
|
||||
return error_message;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
#include "TexturePainting.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "TextureToColor/TextureToColor.hpp"
|
||||
#include "TextureToColor/ColorUtils.hpp"
|
||||
|
||||
#include "Model.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "TriangleSelector.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
static cv::Mat decode_texture_image(const TextureImage& img) {
|
||||
if (img.data.empty())
|
||||
return {};
|
||||
|
||||
// Raw encoded image data (PNG/JPEG) from glTF loader: width == -1
|
||||
if (img.width <= 0 || img.height <= 0) {
|
||||
std::vector<unsigned char> buf(img.data.begin(), img.data.end());
|
||||
cv::Mat raw(1, static_cast<int>(buf.size()), CV_8UC1, buf.data());
|
||||
cv::Mat decoded = cv::imdecode(raw, cv::IMREAD_COLOR);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
int cv_type = (img.channels == 4) ? CV_8UC4 : CV_8UC3;
|
||||
std::vector<unsigned char> pixel_buf(img.data.begin(), img.data.end());
|
||||
cv::Mat src(img.height, img.width, cv_type, pixel_buf.data());
|
||||
|
||||
cv::Mat bgr;
|
||||
if (img.channels == 4)
|
||||
cv::cvtColor(src, bgr, cv::COLOR_RGBA2BGR);
|
||||
else if (img.channels == 3)
|
||||
cv::cvtColor(src, bgr, cv::COLOR_RGB2BGR);
|
||||
else
|
||||
return {};
|
||||
|
||||
return bgr;
|
||||
}
|
||||
|
||||
static void build_tex2color_mesh(
|
||||
const TexturedMesh& textured,
|
||||
tex2color::TriMesh& mesh,
|
||||
std::vector<std::vector<Vec2f>>& uv_coords)
|
||||
{
|
||||
const size_t nv = textured.vertices.size();
|
||||
const size_t nf = textured.indices.size();
|
||||
|
||||
mesh.vertices.resize(nv);
|
||||
for (size_t i = 0; i < nv; ++i) {
|
||||
mesh.vertices[i] = Vec3f(
|
||||
textured.vertices[i][0],
|
||||
textured.vertices[i][1],
|
||||
textured.vertices[i][2]);
|
||||
}
|
||||
|
||||
mesh.indices.resize(nf);
|
||||
for (size_t i = 0; i < nf; ++i) {
|
||||
mesh.indices[i] = Vec3i32(
|
||||
textured.indices[i][0],
|
||||
textured.indices[i][1],
|
||||
textured.indices[i][2]);
|
||||
}
|
||||
|
||||
uv_coords.resize(nf);
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
uv_coords[fi].resize(3);
|
||||
for (int vi = 0; vi < 3; ++vi) {
|
||||
if (textured.has_face_uvs()) {
|
||||
int uv_idx = textured.uv_indices[fi][vi];
|
||||
if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) {
|
||||
uv_coords[fi][vi] = Vec2f(
|
||||
textured.uv_coords[uv_idx][0],
|
||||
textured.uv_coords[uv_idx][1]);
|
||||
} else {
|
||||
uv_coords[fi][vi] = Vec2f(0.f, 0.f);
|
||||
}
|
||||
} else {
|
||||
int vtx_idx = textured.indices[fi][vi];
|
||||
if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) {
|
||||
uv_coords[fi][vi] = Vec2f(
|
||||
textured.uvs[vtx_idx][0],
|
||||
textured.uvs[vtx_idx][1]);
|
||||
} else {
|
||||
uv_coords[fi][vi] = Vec2f(0.f, 0.f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void extract_painted_mesh(
|
||||
const tex2color::TriMesh& color_mesh,
|
||||
const std::vector<std::array<std::size_t,3>>& face_colors,
|
||||
PaintedMesh& painted)
|
||||
{
|
||||
const size_t nv = color_mesh.vertices.size();
|
||||
const size_t nf = color_mesh.indices.size();
|
||||
|
||||
painted.vertices.resize(nv);
|
||||
for (size_t i = 0; i < nv; ++i) {
|
||||
const auto& v = color_mesh.vertices[i];
|
||||
painted.vertices[i] = {v.x(), v.y(), v.z()};
|
||||
}
|
||||
|
||||
painted.indices.resize(nf);
|
||||
for (size_t i = 0; i < nf; ++i) {
|
||||
const auto& f = color_mesh.indices[i];
|
||||
painted.indices[i] = {f[0], f[1], f[2]};
|
||||
}
|
||||
|
||||
painted.face_colors = face_colors;
|
||||
|
||||
std::set<std::array<std::size_t,3>> unique_colors(face_colors.begin(), face_colors.end());
|
||||
painted.cluster_colors.assign(unique_colors.begin(), unique_colors.end());
|
||||
}
|
||||
|
||||
// Build a vertically-stacked atlas from multiple textures and remap per-face UVs.
|
||||
//
|
||||
// Sub-textures are laid out left-aligned (x=0) at successive y offsets, with
|
||||
// atlas_w taken as the maximum width across all sub-textures. UVs must therefore
|
||||
// be remapped on BOTH axes so that faces belonging to a sub-texture narrower
|
||||
// than atlas_w sample inside that sub-texture's region (left side of the atlas)
|
||||
// instead of the right-side zero-padding. Materials that carry only a baseColor
|
||||
// (no map_Kd / glTF baseColorTexture) get their own 1x1 swatch at the bottom of
|
||||
// the atlas so their faces sample the correct flat colour rather than being
|
||||
// silently aliased onto textures[0].
|
||||
static bool build_multi_texture_atlas(
|
||||
const TexturedMesh& textured,
|
||||
cv::Mat& out_atlas,
|
||||
std::vector<std::vector<Vec2f>>& out_uv_coords)
|
||||
{
|
||||
std::vector<cv::Mat> decoded;
|
||||
decoded.reserve(textured.textures.size());
|
||||
for (const auto& ti : textured.textures)
|
||||
decoded.push_back(decode_texture_image(ti));
|
||||
|
||||
const bool has_mapping = !textured.material_texture_map.empty();
|
||||
const size_t nf = textured.indices.size();
|
||||
|
||||
auto resolve_tex_idx = [&](int mat_idx) -> int {
|
||||
if (!has_mapping || mat_idx < 0
|
||||
|| static_cast<size_t>(mat_idx) >= textured.material_texture_map.size())
|
||||
return -1;
|
||||
const int ti = textured.material_texture_map[mat_idx];
|
||||
if (ti < 0 || static_cast<size_t>(ti) >= decoded.size() || decoded[ti].empty())
|
||||
return -1;
|
||||
return ti;
|
||||
};
|
||||
|
||||
// Determine atlas width (max width across all textures) and per-texture row offsets.
|
||||
int atlas_w = 0;
|
||||
int atlas_h = 0;
|
||||
std::vector<int> y_offsets(decoded.size(), 0);
|
||||
int first_usable_tex = -1;
|
||||
for (size_t i = 0; i < decoded.size(); ++i) {
|
||||
if (decoded[i].empty()) continue;
|
||||
if (first_usable_tex < 0) first_usable_tex = static_cast<int>(i);
|
||||
y_offsets[i] = atlas_h;
|
||||
atlas_w = std::max(atlas_w, decoded[i].cols);
|
||||
atlas_h += decoded[i].rows;
|
||||
}
|
||||
if (atlas_w == 0 || atlas_h == 0)
|
||||
return false;
|
||||
|
||||
// Collect materials that have a baseColor but no usable texture so we can
|
||||
// route their faces to a dedicated 1x1 solid swatch instead of aliasing
|
||||
// them onto textures[0].
|
||||
std::map<int, int> mat_solid_y; // mat_idx -> y row in atlas
|
||||
std::map<int, std::array<float,4>> mat_solid_color; // mat_idx -> baseColor (RGBA)
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
|
||||
if (mat_idx < 0) continue;
|
||||
if (resolve_tex_idx(mat_idx) >= 0) continue;
|
||||
if (static_cast<size_t>(mat_idx) >= textured.material_colors.size()) continue;
|
||||
if (mat_solid_y.find(mat_idx) != mat_solid_y.end()) continue;
|
||||
mat_solid_y[mat_idx] = atlas_h++;
|
||||
mat_solid_color[mat_idx] = textured.material_colors[mat_idx];
|
||||
}
|
||||
|
||||
out_atlas = cv::Mat::zeros(atlas_h, atlas_w, CV_8UC3);
|
||||
for (size_t i = 0; i < decoded.size(); ++i) {
|
||||
if (decoded[i].empty()) continue;
|
||||
cv::Mat roi = out_atlas(cv::Rect(0, y_offsets[i], decoded[i].cols, decoded[i].rows));
|
||||
decoded[i].copyTo(roi);
|
||||
}
|
||||
for (const auto& kv : mat_solid_color) {
|
||||
const auto& c = kv.second;
|
||||
// OpenCV stores BGR; baseColor is RGBA in [0,1].
|
||||
out_atlas.at<cv::Vec3b>(mat_solid_y[kv.first], 0) = cv::Vec3b(
|
||||
static_cast<uchar>(std::clamp(c[2] * 255.f, 0.f, 255.f)),
|
||||
static_cast<uchar>(std::clamp(c[1] * 255.f, 0.f, 255.f)),
|
||||
static_cast<uchar>(std::clamp(c[0] * 255.f, 0.f, 255.f)));
|
||||
}
|
||||
|
||||
out_uv_coords.resize(nf);
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
|
||||
const int tex_idx = resolve_tex_idx(mat_idx);
|
||||
|
||||
// Pick the atlas region this face samples from.
|
||||
int y_off = 0, x_off = 0, th = atlas_h, tw = atlas_w;
|
||||
bool use_solid = false;
|
||||
if (tex_idx >= 0) {
|
||||
y_off = y_offsets[tex_idx];
|
||||
th = decoded[tex_idx].rows;
|
||||
tw = decoded[tex_idx].cols;
|
||||
} else if (mat_idx >= 0 && mat_solid_y.count(mat_idx) > 0) {
|
||||
y_off = mat_solid_y[mat_idx];
|
||||
th = 1;
|
||||
tw = 1;
|
||||
use_solid = true;
|
||||
} else if (first_usable_tex >= 0) {
|
||||
// Last-resort fallback: faces without a material or without any
|
||||
// baseColor still need somewhere to sample; the first usable
|
||||
// texture preserves legacy behaviour and, with the per-axis
|
||||
// remapping below, no longer aliases onto the zero-padded right
|
||||
// margin even when sub-textures have unequal widths.
|
||||
y_off = y_offsets[first_usable_tex];
|
||||
th = decoded[first_usable_tex].rows;
|
||||
tw = decoded[first_usable_tex].cols;
|
||||
}
|
||||
|
||||
out_uv_coords[fi].resize(3);
|
||||
for (int vi = 0; vi < 3; ++vi) {
|
||||
float u = 0.f, v = 0.f;
|
||||
if (textured.has_face_uvs()) {
|
||||
int uv_idx = textured.uv_indices[fi][vi];
|
||||
if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) {
|
||||
u = textured.uv_coords[uv_idx][0];
|
||||
v = textured.uv_coords[uv_idx][1];
|
||||
}
|
||||
} else {
|
||||
int vtx_idx = textured.indices[fi][vi];
|
||||
if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) {
|
||||
u = textured.uvs[vtx_idx][0];
|
||||
v = textured.uvs[vtx_idx][1];
|
||||
}
|
||||
}
|
||||
if (use_solid) {
|
||||
// Aim at the centre of the 1x1 swatch so bilinear sampling
|
||||
// (in tex2color) cannot drift into neighbouring rows.
|
||||
const float u_atlas = (x_off + 0.5f) / static_cast<float>(atlas_w);
|
||||
const float v_atlas = (y_off + 0.5f) / static_cast<float>(atlas_h);
|
||||
out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas);
|
||||
} else {
|
||||
// Wrap to [0,1) on both axes (OBJ tile UVs may step outside
|
||||
// the unit square), then scale by the sub-texture extents so
|
||||
// samples land inside its actual region. Without scaling u,
|
||||
// any sub-texture narrower than atlas_w would have all its
|
||||
// faces sampled from the right-side zero-padding.
|
||||
u = u - std::floor(u);
|
||||
v = v - std::floor(v);
|
||||
const float u_atlas = (x_off + u * tw) / static_cast<float>(atlas_w);
|
||||
const float v_atlas = (y_off + v * th) / static_cast<float>(atlas_h);
|
||||
out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool texture_to_painting(
|
||||
const TexturedMesh& textured,
|
||||
PaintedMesh& painted,
|
||||
const TexturePaintingSettings& settings,
|
||||
PaintProgressCallback progress,
|
||||
PaintCancelCallback cancel)
|
||||
{
|
||||
if (textured.vertices.empty() || textured.indices.empty() || textured.textures.empty())
|
||||
return false;
|
||||
|
||||
cv::Mat texture;
|
||||
tex2color::TriMesh input_mesh;
|
||||
std::vector<std::vector<Vec2f>> uv_coords;
|
||||
|
||||
const bool multi_tex = textured.textures.size() > 1 && !textured.material_texture_map.empty();
|
||||
|
||||
if (multi_tex) {
|
||||
if (!build_multi_texture_atlas(textured, texture, uv_coords))
|
||||
return false;
|
||||
// Build mesh geometry (atlas UVs already computed above)
|
||||
const size_t nv = textured.vertices.size();
|
||||
const size_t nf = textured.indices.size();
|
||||
input_mesh.vertices.resize(nv);
|
||||
for (size_t i = 0; i < nv; ++i)
|
||||
input_mesh.vertices[i] = Vec3f(
|
||||
textured.vertices[i][0], textured.vertices[i][1], textured.vertices[i][2]);
|
||||
input_mesh.indices.resize(nf);
|
||||
for (size_t i = 0; i < nf; ++i)
|
||||
input_mesh.indices[i] = Vec3i32(
|
||||
textured.indices[i][0], textured.indices[i][1], textured.indices[i][2]);
|
||||
} else {
|
||||
texture = decode_texture_image(textured.textures[0]);
|
||||
if (texture.empty())
|
||||
return false;
|
||||
build_tex2color_mesh(textured, input_mesh, uv_coords);
|
||||
}
|
||||
|
||||
tex2color::TextureToColorSettings algo_settings;
|
||||
algo_settings.target_colors_num = settings.target_colors_num;
|
||||
algo_settings.smooth_weight = settings.smooth_weight;
|
||||
algo_settings.oversampling_iters = settings.oversampling_iters;
|
||||
switch (settings.mesh_repair_decision) {
|
||||
case TexturePaintingSettings::MeshRepairDecision::Ask:
|
||||
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask;
|
||||
break;
|
||||
case TexturePaintingSettings::MeshRepairDecision::RepairAndImport:
|
||||
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport;
|
||||
break;
|
||||
case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair:
|
||||
default:
|
||||
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair;
|
||||
break;
|
||||
}
|
||||
|
||||
tex2color::AlgoProgressCallback algo_progress = nullptr;
|
||||
if (progress) {
|
||||
algo_progress = [&progress](tex2color::AlgoProgress p) {
|
||||
progress(p.percent, p.message);
|
||||
};
|
||||
}
|
||||
|
||||
tex2color::AlgoCancelCallback algo_cancel = nullptr;
|
||||
if (cancel) {
|
||||
algo_cancel = [&cancel]() -> bool { return cancel(); };
|
||||
}
|
||||
|
||||
tex2color::TriMesh color_mesh;
|
||||
std::vector<std::array<std::size_t,3>> face_colors;
|
||||
algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required;
|
||||
algo_settings.mesh_repair_callback = settings.mesh_repair_callback;
|
||||
|
||||
bool ok = tex2color::TextureToColor(
|
||||
input_mesh, uv_coords, texture,
|
||||
color_mesh, face_colors,
|
||||
algo_settings, algo_progress, algo_cancel);
|
||||
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
extract_painted_mesh(color_mesh, face_colors, painted);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool face_colors_to_painting(
|
||||
const TexturedMesh& mesh,
|
||||
PaintedMesh& painted,
|
||||
const TexturePaintingSettings& settings,
|
||||
PaintProgressCallback progress,
|
||||
PaintCancelCallback cancel)
|
||||
{
|
||||
if (mesh.vertices.empty() || mesh.indices.empty() || mesh.precomputed_face_colors.empty())
|
||||
return false;
|
||||
|
||||
// Build tex2color::TriMesh from input geometry
|
||||
tex2color::TriMesh input_mesh;
|
||||
input_mesh.vertices.resize(mesh.vertices.size());
|
||||
for (size_t i = 0; i < mesh.vertices.size(); ++i)
|
||||
input_mesh.vertices[i] = Vec3f(mesh.vertices[i][0], mesh.vertices[i][1], mesh.vertices[i][2]);
|
||||
input_mesh.indices.resize(mesh.indices.size());
|
||||
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
||||
input_mesh.indices[i] = Vec3i32(mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2]);
|
||||
|
||||
// Forward settings to tex2color
|
||||
tex2color::TextureToColorSettings algo_settings;
|
||||
algo_settings.target_colors_num = settings.target_colors_num;
|
||||
algo_settings.smooth_weight = settings.smooth_weight;
|
||||
switch (settings.mesh_repair_decision) {
|
||||
case TexturePaintingSettings::MeshRepairDecision::Ask:
|
||||
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask;
|
||||
break;
|
||||
case TexturePaintingSettings::MeshRepairDecision::RepairAndImport:
|
||||
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport;
|
||||
break;
|
||||
case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair:
|
||||
default:
|
||||
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair;
|
||||
break;
|
||||
}
|
||||
algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required;
|
||||
algo_settings.mesh_repair_callback = settings.mesh_repair_callback;
|
||||
|
||||
tex2color::AlgoProgressCallback algo_progress = nullptr;
|
||||
if (progress) {
|
||||
algo_progress = [&progress](tex2color::AlgoProgress p) {
|
||||
progress(p.percent, p.message);
|
||||
};
|
||||
}
|
||||
tex2color::AlgoCancelCallback algo_cancel = nullptr;
|
||||
if (cancel) {
|
||||
algo_cancel = [&cancel]() -> bool { return cancel(); };
|
||||
}
|
||||
|
||||
tex2color::TriMesh out_mesh;
|
||||
std::vector<std::array<std::size_t,3>> out_face_colors;
|
||||
bool ok = tex2color::ClusterAndSmooth(
|
||||
input_mesh, mesh.precomputed_face_colors, out_mesh, out_face_colors,
|
||||
algo_settings, algo_progress, algo_cancel,
|
||||
mesh.precomputed_vertex_colors);
|
||||
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
extract_painted_mesh(out_mesh, out_face_colors, painted);
|
||||
return true;
|
||||
}
|
||||
|
||||
double compute_delta_e(
|
||||
const std::array<std::size_t,3>& rgb1,
|
||||
const std::array<float,4>& rgba2)
|
||||
{
|
||||
return tex2color::color_utils::calc_rgb_color_difference_by_ciede2000(
|
||||
rgb1,
|
||||
{
|
||||
static_cast<std::size_t>(rgba2[0] * 255.0f),
|
||||
static_cast<std::size_t>(rgba2[1] * 255.0f),
|
||||
static_cast<std::size_t>(rgba2[2] * 255.0f)
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<FilamentMatch> match_clusters_to_filaments(
|
||||
const std::vector<std::array<std::size_t,3>>& cluster_colors,
|
||||
const std::vector<std::array<float,4>>& filament_colors,
|
||||
const std::vector<std::string>& /*filament_names*/)
|
||||
{
|
||||
std::vector<FilamentMatch> matches(cluster_colors.size());
|
||||
|
||||
for (size_t ci = 0; ci < cluster_colors.size(); ++ci) {
|
||||
matches[ci].cluster_index = static_cast<int>(ci);
|
||||
matches[ci].cluster_color = cluster_colors[ci];
|
||||
matches[ci].delta_e = 1e9;
|
||||
|
||||
for (size_t fi = 0; fi < filament_colors.size(); ++fi) {
|
||||
double de = compute_delta_e(cluster_colors[ci], filament_colors[fi]);
|
||||
if (de < matches[ci].delta_e) {
|
||||
matches[ci].delta_e = de;
|
||||
matches[ci].filament_index = static_cast<int>(fi);
|
||||
matches[ci].filament_color = filament_colors[fi];
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
bool apply_painted_mesh_to_volume(
|
||||
const PaintedMesh& painted,
|
||||
const std::vector<FilamentMatch>& matches,
|
||||
ModelVolume& volume)
|
||||
{
|
||||
if (painted.face_colors.empty() || matches.empty())
|
||||
return false;
|
||||
|
||||
const auto& cluster_colors = painted.cluster_colors;
|
||||
std::map<std::array<std::size_t,3>, int> color_to_filament;
|
||||
for (const auto& m : matches) {
|
||||
if (m.cluster_index >= 0 && m.cluster_index < (int)cluster_colors.size() && m.filament_index >= 0)
|
||||
color_to_filament[cluster_colors[m.cluster_index]] = m.filament_index;
|
||||
}
|
||||
|
||||
indexed_triangle_set its;
|
||||
its.vertices.resize(painted.vertices.size());
|
||||
for (size_t i = 0; i < painted.vertices.size(); ++i) {
|
||||
its.vertices[i] = Vec3f(
|
||||
painted.vertices[i][0],
|
||||
painted.vertices[i][1],
|
||||
painted.vertices[i][2]);
|
||||
}
|
||||
its.indices.resize(painted.indices.size());
|
||||
for (size_t i = 0; i < painted.indices.size(); ++i) {
|
||||
its.indices[i] = Vec3i32(
|
||||
painted.indices[i][0],
|
||||
painted.indices[i][1],
|
||||
painted.indices[i][2]);
|
||||
}
|
||||
|
||||
TriangleMesh new_mesh(std::move(its));
|
||||
|
||||
// The volume already went through ModelObject::add_volume ->
|
||||
// center_geometry_after_creation, which translated its mesh by
|
||||
// -source.mesh_offset (and folded that shift into the volume
|
||||
// transformation). The painted mesh, however, is derived from the
|
||||
// raw textured mesh and is therefore expressed in the original
|
||||
// un-centered coordinate frame. Reuse the exact recorded shift to
|
||||
// align it -- do NOT compute it from the bounding-box centers of
|
||||
// the two meshes: tex2color::TextureToColor performs subdivision
|
||||
// and CGAL polygon-soup repair, so the painted vertex count and
|
||||
// bbox no longer match the original textured mesh and a bbox-
|
||||
// center alignment would silently displace the geometry.
|
||||
//
|
||||
// If the model has been scaled by Model::convert_from_meters /
|
||||
// convert_from_imperial_units after load, the painted mesh fed
|
||||
// here is already in millimetres (Model::convert_* also scales
|
||||
// texture_mesh in place) while source.mesh_offset was recorded
|
||||
// before the conversion and therefore still lives in the original
|
||||
// pre-scaled frame. Bring it into the same frame as the painted
|
||||
// vertices so the alignment shift below stays correct on the
|
||||
// textured-import path. This compensation is scoped to this
|
||||
// function so that other (non-textured) import paths are not
|
||||
// affected.
|
||||
Vec3d mesh_offset = volume.source.mesh_offset;
|
||||
double unit_scale = 1.0;
|
||||
if (volume.source.is_converted_from_meters)
|
||||
unit_scale = 1000.0;
|
||||
else if (volume.source.is_converted_from_inches)
|
||||
unit_scale = 25.4;
|
||||
if (unit_scale != 1.0)
|
||||
mesh_offset *= unit_scale;
|
||||
|
||||
if (!mesh_offset.isApprox(Vec3d::Zero()))
|
||||
new_mesh.translate(-mesh_offset.cast<float>());
|
||||
new_mesh.set_init_shift(mesh_offset);
|
||||
|
||||
// Log bbox drift for diagnostics. Subdivision + CGAL polygon-soup
|
||||
// repair routinely changes vertex count and bbox, so moderate drift
|
||||
// is expected and must not block the apply.
|
||||
if (!new_mesh.empty() && !volume.mesh().empty()) {
|
||||
const Vec3d new_center = new_mesh.bounding_box().center();
|
||||
const Vec3d cur_center = volume.mesh().bounding_box().center();
|
||||
const double diag = volume.mesh().bounding_box().size().norm();
|
||||
const double drift = (new_center - cur_center).norm();
|
||||
if (drift > 0.05 * std::max(1.0, diag))
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "apply_painted_mesh_to_volume: painted bbox center drifted by "
|
||||
<< drift << " (bbox diag=" << diag
|
||||
<< ", unit_scale=" << unit_scale
|
||||
<< ", from_meters=" << volume.source.is_converted_from_meters
|
||||
<< ", from_inches=" << volume.source.is_converted_from_inches << ")";
|
||||
else if (drift > 1e-3 * std::max(1.0, diag))
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "apply_painted_mesh_to_volume: minor bbox drift "
|
||||
<< drift << " (bbox diag=" << diag
|
||||
<< ", unit_scale=" << unit_scale << ")";
|
||||
}
|
||||
|
||||
volume.set_mesh(std::move(new_mesh));
|
||||
volume.calculate_convex_hull();
|
||||
|
||||
// Re-center the replaced mesh so its bbox center sits at the origin,
|
||||
// matching what center_geometry_after_creation did for the original mesh.
|
||||
// CGAL repair / subdivision may shift the bbox center (drift); without
|
||||
// re-centering, the volume offset (which was computed for the original
|
||||
// centered mesh) no longer matches, causing the model to float or clip.
|
||||
// Pass false to keep source.mesh_offset unchanged.
|
||||
volume.center_geometry_after_creation(false);
|
||||
volume.invalidate_convex_hull_2d();
|
||||
|
||||
// Mesh geometry has been replaced; any per-face annotation indexed
|
||||
// against the previous triangle set is now stale. mmu_segmentation_facets
|
||||
// is rewritten below from the new selector; reset the others so future
|
||||
// import paths that carry support / seam / fuzzy_skin painting cannot
|
||||
// leak indices from the old mesh into the new one.
|
||||
volume.supported_facets.reset();
|
||||
volume.fuzzy_skin_facets.reset();
|
||||
volume.seam_facets.reset();
|
||||
|
||||
if (ModelObject* obj = volume.get_object())
|
||||
obj->invalidate_bounding_box();
|
||||
|
||||
TriangleSelector selector(volume.mesh());
|
||||
for (size_t fi = 0; fi < painted.face_colors.size() && fi < (size_t)volume.mesh().its.indices.size(); ++fi) {
|
||||
auto it = color_to_filament.find(painted.face_colors[fi]);
|
||||
if (it != color_to_filament.end()) {
|
||||
int extruder_idx = it->second;
|
||||
auto state = static_cast<EnforcerBlockerType>(
|
||||
static_cast<int>(EnforcerBlockerType::Extruder1) + extruder_idx);
|
||||
if (state <= EnforcerBlockerType::ExtruderMax)
|
||||
selector.set_facet(static_cast<int>(fi), state);
|
||||
}
|
||||
}
|
||||
|
||||
volume.mmu_segmentation_facets.set(selector);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_texture_to_pixels(
|
||||
const TextureImage& img,
|
||||
std::vector<unsigned char>& out_pixels,
|
||||
int& out_w, int& out_h)
|
||||
{
|
||||
cv::Mat decoded = decode_texture_image(img);
|
||||
if (decoded.empty())
|
||||
return false;
|
||||
|
||||
// decoded is BGR, CV_8UC3
|
||||
out_w = decoded.cols;
|
||||
out_h = decoded.rows;
|
||||
size_t nbytes = (size_t)out_w * out_h * 3;
|
||||
out_pixels.resize(nbytes);
|
||||
|
||||
if (decoded.isContinuous()) {
|
||||
std::memcpy(out_pixels.data(), decoded.data, nbytes);
|
||||
} else {
|
||||
for (int r = 0; r < out_h; ++r)
|
||||
std::memcpy(out_pixels.data() + r * out_w * 3, decoded.ptr(r), out_w * 3);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sample face color from texture using 3 explicit UV values (centroid + bilinear).
|
||||
static std::array<std::size_t,3> sample_face_from_uvs(
|
||||
const cv::Mat& tex,
|
||||
const std::array<float,2>& uv0,
|
||||
const std::array<float,2>& uv1,
|
||||
const std::array<float,2>& uv2)
|
||||
{
|
||||
float cu = (uv0[0] + uv1[0] + uv2[0]) / 3.f;
|
||||
float cv_val = (uv0[1] + uv1[1] + uv2[1]) / 3.f;
|
||||
|
||||
cu = cu - std::floor(cu);
|
||||
cv_val = cv_val - std::floor(cv_val);
|
||||
|
||||
float fx = cu * (tex.cols - 1);
|
||||
float fy = cv_val * (tex.rows - 1);
|
||||
|
||||
int x0 = std::clamp(static_cast<int>(fx), 0, tex.cols - 1);
|
||||
int y0 = std::clamp(static_cast<int>(fy), 0, tex.rows - 1);
|
||||
int x1 = std::min(x0 + 1, tex.cols - 1);
|
||||
int y1 = std::min(y0 + 1, tex.rows - 1);
|
||||
|
||||
float wx = fx - x0;
|
||||
float wy = fy - y0;
|
||||
|
||||
const int ch = tex.channels();
|
||||
auto sample = [&](int row, int col) -> std::array<float,3> {
|
||||
const uchar* ptr = tex.data + row * tex.step[0] + col * ch;
|
||||
return {static_cast<float>(ptr[2]), static_cast<float>(ptr[1]), static_cast<float>(ptr[0])};
|
||||
};
|
||||
|
||||
auto c00 = sample(y0, x0);
|
||||
auto c10 = sample(y0, x1);
|
||||
auto c01 = sample(y1, x0);
|
||||
auto c11 = sample(y1, x1);
|
||||
|
||||
std::array<std::size_t,3> color;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
float top = c00[i] * (1.f - wx) + c10[i] * wx;
|
||||
float bot = c01[i] * (1.f - wx) + c11[i] * wx;
|
||||
color[i] = static_cast<std::size_t>(std::clamp(top * (1.f - wy) + bot * wy, 0.f, 255.f));
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// Legacy overload: look up UVs from per-vertex array by vertex indices.
|
||||
static std::array<std::size_t,3> sample_face_from_texture(
|
||||
const cv::Mat& tex,
|
||||
const std::vector<std::array<float,2>>& uvs,
|
||||
const std::array<int,3>& face)
|
||||
{
|
||||
std::array<float,2> uv0 = {0.f, 0.f}, uv1 = {0.f, 0.f}, uv2 = {0.f, 0.f};
|
||||
if (face[0] >= 0 && static_cast<size_t>(face[0]) < uvs.size()) uv0 = uvs[face[0]];
|
||||
if (face[1] >= 0 && static_cast<size_t>(face[1]) < uvs.size()) uv1 = uvs[face[1]];
|
||||
if (face[2] >= 0 && static_cast<size_t>(face[2]) < uvs.size()) uv2 = uvs[face[2]];
|
||||
return sample_face_from_uvs(tex, uv0, uv1, uv2);
|
||||
}
|
||||
|
||||
bool sample_original_face_colors(
|
||||
const TexturedMesh& textured,
|
||||
std::vector<std::array<std::size_t,3>>& out_face_colors)
|
||||
{
|
||||
if (textured.indices.empty())
|
||||
return false;
|
||||
|
||||
// Decode all textures up front
|
||||
std::vector<cv::Mat> decoded_textures;
|
||||
decoded_textures.reserve(textured.textures.size());
|
||||
for (const auto& ti : textured.textures) {
|
||||
decoded_textures.push_back(decode_texture_image(ti));
|
||||
}
|
||||
|
||||
const bool has_mapping = !textured.material_texture_map.empty();
|
||||
const size_t nf = textured.indices.size();
|
||||
out_face_colors.resize(nf);
|
||||
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
|
||||
|
||||
int tex_idx = -1;
|
||||
if (has_mapping && mat_idx >= 0 && static_cast<size_t>(mat_idx) < textured.material_texture_map.size())
|
||||
tex_idx = textured.material_texture_map[mat_idx];
|
||||
else if (!decoded_textures.empty())
|
||||
tex_idx = 0; // fallback: single-texture model
|
||||
|
||||
if (tex_idx >= 0 && static_cast<size_t>(tex_idx) < decoded_textures.size()
|
||||
&& !decoded_textures[tex_idx].empty()) {
|
||||
if (textured.has_face_uvs()) {
|
||||
const auto& ui = textured.uv_indices[fi];
|
||||
auto get_uv = [&](int vi) -> std::array<float,2> {
|
||||
int idx = ui[vi];
|
||||
if (idx >= 0 && static_cast<size_t>(idx) < textured.uv_coords.size())
|
||||
return textured.uv_coords[idx];
|
||||
return {0.f, 0.f};
|
||||
};
|
||||
out_face_colors[fi] = sample_face_from_uvs(
|
||||
decoded_textures[tex_idx], get_uv(0), get_uv(1), get_uv(2));
|
||||
} else {
|
||||
out_face_colors[fi] = sample_face_from_texture(
|
||||
decoded_textures[tex_idx], textured.uvs, textured.indices[fi]);
|
||||
}
|
||||
} else if (has_mapping && mat_idx >= 0
|
||||
&& static_cast<size_t>(mat_idx) < textured.material_colors.size()) {
|
||||
// No texture — use baseColorFactor as solid color
|
||||
const auto& c = textured.material_colors[mat_idx];
|
||||
out_face_colors[fi] = {
|
||||
static_cast<std::size_t>(std::clamp(c[0] * 255.f, 0.f, 255.f)),
|
||||
static_cast<std::size_t>(std::clamp(c[1] * 255.f, 0.f, 255.f)),
|
||||
static_cast<std::size_t>(std::clamp(c[2] * 255.f, 0.f, 255.f))
|
||||
};
|
||||
} else {
|
||||
out_face_colors[fi] = {192, 192, 192}; // default gray
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct indexed_triangle_set;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
class ModelVolume;
|
||||
|
||||
struct TextureImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 4;
|
||||
std::vector<unsigned char> data;
|
||||
};
|
||||
|
||||
struct TexturedMesh {
|
||||
std::vector<std::array<float,3>> vertices;
|
||||
std::vector<std::array<int,3>> indices;
|
||||
std::vector<std::array<float,2>> uvs;
|
||||
std::vector<TextureImage> textures;
|
||||
std::vector<int> material_ids;
|
||||
// material index -> index in textures[] (-1 if no texture, use material_colors)
|
||||
std::vector<int> material_texture_map;
|
||||
// per-material baseColorFactor (RGBA 0-1), indexed by material index
|
||||
std::vector<std::array<float,4>> material_colors;
|
||||
|
||||
// Per-face independent UV support (for OBJ where the same vertex can have
|
||||
// different texture coordinates on different faces).
|
||||
std::vector<std::array<float,2>> uv_coords; // UV coordinate pool
|
||||
std::vector<std::array<int,3>> uv_indices; // per-face UV indices into uv_coords
|
||||
|
||||
bool has_face_uvs() const { return !uv_indices.empty() && !uv_coords.empty(); }
|
||||
|
||||
// Pre-computed per-face colors (e.g. from OBJ vertex colors or MTL Kd).
|
||||
// When non-empty, the pipeline skips texture decode/sample/oversample and
|
||||
// consumes these instead of sampling a texture.
|
||||
// Each entry is {R, G, B} in [0..255].
|
||||
std::vector<std::array<std::size_t,3>> precomputed_face_colors;
|
||||
|
||||
// Per-vertex colors from OBJ (RGBA, [0..1]), indexed by vertex index.
|
||||
// On a low-poly mesh these are quantized into a small palette and the mesh is
|
||||
// split along the resulting cluster boundaries, so color borders stay sharp
|
||||
// instead of being averaged away into a single color per face.
|
||||
std::vector<std::array<float,4>> precomputed_vertex_colors;
|
||||
};
|
||||
|
||||
struct PaintedMesh {
|
||||
std::vector<std::array<float,3>> vertices;
|
||||
std::vector<std::array<int,3>> indices;
|
||||
std::vector<std::array<std::size_t,3>> face_colors; // per-face RGB [0..255]
|
||||
std::vector<std::array<std::size_t,3>> cluster_colors;
|
||||
};
|
||||
|
||||
using PaintProgressCallback = std::function<void(int percent, const char* message)>;
|
||||
using PaintCancelCallback = std::function<bool()>;
|
||||
using PaintMeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh,
|
||||
indexed_triangle_set& repaired_mesh,
|
||||
std::function<void(const char* message, unsigned progress)> progress_callback,
|
||||
std::function<bool()> cancel_callback,
|
||||
std::string* error_message)>;
|
||||
|
||||
struct TexturePaintingSettings {
|
||||
std::size_t target_colors_num = 4;
|
||||
double smooth_weight = 0.5;
|
||||
std::size_t oversampling_iters = 0;
|
||||
enum class MeshRepairDecision {
|
||||
Ask,
|
||||
ImportWithoutRepair,
|
||||
RepairAndImport
|
||||
};
|
||||
MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair;
|
||||
bool* mesh_repair_decision_required = nullptr;
|
||||
PaintMeshRepairCallback mesh_repair_callback;
|
||||
};
|
||||
|
||||
struct FilamentMatch {
|
||||
int cluster_index = -1;
|
||||
int filament_index = -1;
|
||||
double delta_e = 0.0;
|
||||
std::array<std::size_t,3> cluster_color = {0,0,0};
|
||||
std::array<float,4> filament_color = {0,0,0,1};
|
||||
};
|
||||
|
||||
bool texture_to_painting(
|
||||
const TexturedMesh& textured,
|
||||
PaintedMesh& painted,
|
||||
const TexturePaintingSettings& settings = {},
|
||||
PaintProgressCallback progress = nullptr,
|
||||
PaintCancelCallback cancel = nullptr);
|
||||
// Turn pre-computed per-face colors into a painted mesh, skipping texture decode
|
||||
// and UV sampling. A low-poly mesh that also carries precomputed_vertex_colors is
|
||||
// split along quantized color boundaries, which replaces its geometry.
|
||||
bool face_colors_to_painting(
|
||||
const TexturedMesh& mesh,
|
||||
PaintedMesh& painted,
|
||||
const TexturePaintingSettings& settings = {},
|
||||
PaintProgressCallback progress = nullptr,
|
||||
PaintCancelCallback cancel = nullptr);
|
||||
|
||||
|
||||
std::vector<FilamentMatch> match_clusters_to_filaments(
|
||||
const std::vector<std::array<std::size_t,3>>& cluster_colors,
|
||||
const std::vector<std::array<float,4>>& filament_colors,
|
||||
const std::vector<std::string>& filament_names);
|
||||
|
||||
double compute_delta_e(
|
||||
const std::array<std::size_t,3>& rgb1,
|
||||
const std::array<float,4>& rgba2);
|
||||
|
||||
bool apply_painted_mesh_to_volume(
|
||||
const PaintedMesh& painted,
|
||||
const std::vector<FilamentMatch>& matches,
|
||||
ModelVolume& volume);
|
||||
|
||||
// Decode a TextureImage (which may contain raw PNG/JPEG bytes) into BGR pixel data.
|
||||
// On success, populates out_pixels (BGR, 3 bytes/pixel) and sets out_w/out_h.
|
||||
bool decode_texture_to_pixels(
|
||||
const TextureImage& img,
|
||||
std::vector<unsigned char>& out_pixels,
|
||||
int& out_w, int& out_h);
|
||||
|
||||
// Sample per-face colors from the correct texture per material_ids.
|
||||
// Uses material_texture_map / material_colors for multi-material GLBs.
|
||||
// Falls back to textures[0] when the mapping is absent.
|
||||
bool sample_original_face_colors(
|
||||
const TexturedMesh& textured,
|
||||
std::vector<std::array<std::size_t,3>>& out_face_colors);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
|
||||
namespace Slic3r { namespace tex2color {
|
||||
|
||||
struct AlgoProgress {
|
||||
int percent = 0;
|
||||
const char* message = "";
|
||||
};
|
||||
|
||||
using AlgoProgressCallback = std::function<void(AlgoProgress)>;
|
||||
using AlgoCancelCallback = std::function<bool()>;
|
||||
|
||||
} // namespace tex2color
|
||||
} // namespace Slic3r
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,252 @@
|
||||
#pragma once
|
||||
#include "TriMesh.hpp"
|
||||
#include "CgalUtils.hpp"
|
||||
#include "Callbacks.hpp"
|
||||
#include <CGAL/Polygon_mesh_processing/border.h>
|
||||
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair.h>
|
||||
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
|
||||
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
|
||||
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>
|
||||
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r { namespace tex2color {
|
||||
|
||||
namespace PMP = CGAL::Polygon_mesh_processing;
|
||||
|
||||
// Default upper bound on the number of half-edges in any single boundary cycle
|
||||
// that CloseBoundariesAndRepairManifoldness will attempt to triangulate. The
|
||||
// cost of triangulate_hole grows non-linearly with cycle length, so this caps
|
||||
// the worst-case per-hole work rather than the aggregate boundary size: a mesh
|
||||
// with many small holes is still fully repaired, while a mesh containing one
|
||||
// pathologically large hole skips triangulation entirely.
|
||||
inline constexpr std::size_t MAX_REPAIRABLE_MESH_HOLE_EDGES = 500;
|
||||
|
||||
// Default upper bound on the aggregate number of boundary half-edges in the
|
||||
// mesh (summed across every boundary cycle). When the total boundary length is
|
||||
// excessive, even if each individual cycle is short, triangulating all of them
|
||||
// usually indicates a severely fragmented input (e.g. heavily damaged scans)
|
||||
// and rarely yields a usable result, so we skip hole closing entirely.
|
||||
inline constexpr std::size_t MAX_REPAIRABLE_MESH_BOUNDARY_EDGES = 5000;
|
||||
|
||||
struct RepairSetting
|
||||
{
|
||||
// Skip triangulating a boundary cycle whose half-edge count exceeds this.
|
||||
std::size_t max_hole_edges = MAX_REPAIRABLE_MESH_HOLE_EDGES;
|
||||
// Skip hole closing entirely when the total boundary half-edge count
|
||||
// (summed across all cycles) exceeds this.
|
||||
std::size_t max_boundary_edges = MAX_REPAIRABLE_MESH_BOUNDARY_EDGES;
|
||||
};
|
||||
|
||||
struct BoundaryEdgeStats
|
||||
{
|
||||
std::size_t total_boundary_edges = 0;
|
||||
std::size_t max_cycle_edges = 0;
|
||||
std::size_t cycle_count = 0;
|
||||
};
|
||||
|
||||
// Read-only inspection of the mesh's boundary cycles. Caller is responsible for
|
||||
// any pre-processing (e.g. stitch_borders) needed for the count to be meaningful.
|
||||
inline BoundaryEdgeStats ComputeBoundaryEdgeStats(const cgalutils::CGALMesh& cgal_mesh)
|
||||
{
|
||||
using CGALMesh = cgalutils::CGALMesh;
|
||||
using HalfedgeDescriptor = boost::graph_traits<CGALMesh>::halfedge_descriptor;
|
||||
|
||||
std::vector<HalfedgeDescriptor> border_cycles;
|
||||
PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles));
|
||||
|
||||
BoundaryEdgeStats stats;
|
||||
stats.cycle_count = border_cycles.size();
|
||||
for (const HalfedgeDescriptor h0 : border_cycles) {
|
||||
std::size_t len = 0;
|
||||
HalfedgeDescriptor h = h0;
|
||||
do {
|
||||
++len;
|
||||
h = next(h, cgal_mesh);
|
||||
} while (h != h0);
|
||||
stats.max_cycle_edges = std::max(stats.max_cycle_edges, len);
|
||||
stats.total_boundary_edges += len;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Unconditionally close every boundary cycle of the mesh and repair non-manifold
|
||||
// vertices. The caller (e.g. RepairMesh) is expected to gate this call based on
|
||||
// boundary statistics; entering this function always triggers triangulation.
|
||||
inline void CloseBoundariesAndRepairManifoldness(cgalutils::CGALMesh& cgal_mesh)
|
||||
{
|
||||
using CGALMesh = cgalutils::CGALMesh;
|
||||
using HalfedgeDescriptor = boost::graph_traits<CGALMesh>::halfedge_descriptor;
|
||||
using FaceDescriptor = boost::graph_traits<CGALMesh>::face_descriptor;
|
||||
|
||||
PMP::stitch_borders(cgal_mesh);
|
||||
PMP::duplicate_non_manifold_vertices(cgal_mesh);
|
||||
|
||||
std::vector<HalfedgeDescriptor> border_cycles;
|
||||
PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles));
|
||||
|
||||
for (const HalfedgeDescriptor h : border_cycles) {
|
||||
std::vector<FaceDescriptor> patch_faces;
|
||||
PMP::triangulate_hole(cgal_mesh, h, std::back_inserter(patch_faces));
|
||||
}
|
||||
|
||||
PMP::remove_degenerate_faces(cgal_mesh);
|
||||
PMP::duplicate_non_manifold_vertices(cgal_mesh);
|
||||
}
|
||||
|
||||
inline bool RepairMesh(const TriMesh& mesh,
|
||||
std::shared_ptr<TriMesh>& out_mesh,
|
||||
AlgoProgressCallback progress_callback = nullptr,
|
||||
AlgoCancelCallback cancel_callback = nullptr,
|
||||
const RepairSetting& setting = RepairSetting{})
|
||||
{
|
||||
using Clock = std::chrono::steady_clock;
|
||||
auto elapsed_ms = [](Clock::time_point t0) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
|
||||
};
|
||||
|
||||
const Clock::time_point t_total = Clock::now();
|
||||
|
||||
// Convert TriMesh to polygon soup (point container + triangle index container)
|
||||
std::vector<cgalutils::Kernel::Point_3> soup_points;
|
||||
std::vector<std::vector<std::size_t>> soup_triangles;
|
||||
|
||||
soup_points.reserve(mesh.vertices.size());
|
||||
for (const TriVertex& v : mesh.vertices) {
|
||||
soup_points.emplace_back(v.x(), v.y(), v.z());
|
||||
}
|
||||
|
||||
soup_triangles.reserve(mesh.indices.size());
|
||||
for (const TriFace& f : mesh.indices) {
|
||||
soup_triangles.push_back({static_cast<std::size_t>(f[0]),
|
||||
static_cast<std::size_t>(f[1]),
|
||||
static_cast<std::size_t>(f[2])});
|
||||
}
|
||||
|
||||
if (progress_callback) {
|
||||
progress_callback({30, "Repairing polygon soup"});
|
||||
}
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
const auto t0 = Clock::now();
|
||||
PMP::repair_polygon_soup(soup_points, soup_triangles);
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=repair_polygon_soup took="
|
||||
<< elapsed_ms(t0) << " ms";
|
||||
}
|
||||
|
||||
if (progress_callback) {
|
||||
progress_callback({50, "Orienting polygon soup"});
|
||||
}
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
const auto t0 = Clock::now();
|
||||
PMP::orient_polygon_soup(soup_points, soup_triangles);
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=orient_polygon_soup took="
|
||||
<< elapsed_ms(t0) << " ms";
|
||||
}
|
||||
|
||||
if (progress_callback) {
|
||||
progress_callback({70, "Converting to CGAL mesh"});
|
||||
}
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cgalutils::CGALMesh cgal_mesh;
|
||||
{
|
||||
const auto t0 = Clock::now();
|
||||
PMP::polygon_soup_to_polygon_mesh(soup_points, soup_triangles, cgal_mesh);
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=polygon_soup_to_polygon_mesh took="
|
||||
<< elapsed_ms(t0) << " ms";
|
||||
}
|
||||
|
||||
{
|
||||
const auto t0 = Clock::now();
|
||||
PMP::remove_degenerate_faces(cgal_mesh);
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=remove_degenerate_faces took="
|
||||
<< elapsed_ms(t0) << " ms";
|
||||
}
|
||||
|
||||
if (progress_callback) {
|
||||
progress_callback({80, "Closing mesh boundaries"});
|
||||
}
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stitch borders and duplicate non-manifold vertices first so that the
|
||||
// boundary statistics below reflect the post-stitch topology; otherwise
|
||||
// boundaries that would close on stitching inflate the counts and may
|
||||
// cause the gate to skip hole filling unnecessarily.
|
||||
BoundaryEdgeStats stats;
|
||||
{
|
||||
const auto t0 = Clock::now();
|
||||
PMP::stitch_borders(cgal_mesh);
|
||||
PMP::duplicate_non_manifold_vertices(cgal_mesh);
|
||||
stats = ComputeBoundaryEdgeStats(cgal_mesh);
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=boundary_stats took="
|
||||
<< elapsed_ms(t0) << " ms"
|
||||
<< " total_boundary_edges=" << stats.total_boundary_edges
|
||||
<< " max_cycle_edges=" << stats.max_cycle_edges
|
||||
<< " cycle_count=" << stats.cycle_count;
|
||||
}
|
||||
|
||||
const bool can_repair_holes =
|
||||
stats.total_boundary_edges <= setting.max_boundary_edges &&
|
||||
stats.max_cycle_edges <= setting.max_hole_edges;
|
||||
|
||||
if (can_repair_holes) {
|
||||
const auto t0 = Clock::now();
|
||||
CloseBoundariesAndRepairManifoldness(cgal_mesh);
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=close_boundaries took="
|
||||
<< elapsed_ms(t0) << " ms";
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "TextureToColor: RepairMesh skip hole closing"
|
||||
<< ", total_boundary_edges=" << stats.total_boundary_edges
|
||||
<< " (limit=" << setting.max_boundary_edges << ")"
|
||||
<< ", max_cycle_edges=" << stats.max_cycle_edges
|
||||
<< " (limit=" << setting.max_hole_edges << ")"
|
||||
<< ", cycle_count=" << stats.cycle_count;
|
||||
}
|
||||
|
||||
if (progress_callback) {
|
||||
progress_callback({85, "Converting from CGAL mesh"});
|
||||
}
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<TriMesh> out;
|
||||
{
|
||||
const auto t0 = Clock::now();
|
||||
out = std::make_shared<TriMesh>(cgalutils::cgal_to_trimesh(cgal_mesh));
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=cgal_to_trimesh took="
|
||||
<< elapsed_ms(t0) << " ms";
|
||||
}
|
||||
|
||||
out_mesh = std::move(out);
|
||||
if (progress_callback) {
|
||||
progress_callback({100, "Done"});
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh total=" << elapsed_ms(t_total) << " ms";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace tex2color
|
||||
} // namespace Slic3r
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include "Callbacks.hpp"
|
||||
#include "TriMesh.hpp"
|
||||
#include "opencv2/core.hpp"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r { namespace tex2color {
|
||||
|
||||
enum class MeshRepairDecision {
|
||||
Ask,
|
||||
ImportWithoutRepair,
|
||||
RepairAndImport
|
||||
};
|
||||
|
||||
using MeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh,
|
||||
indexed_triangle_set& repaired_mesh,
|
||||
std::function<void(const char* message, unsigned progress)> progress_callback,
|
||||
std::function<bool()> cancel_callback,
|
||||
std::string* error_message)>;
|
||||
|
||||
struct TextureToColorSettings {
|
||||
std::size_t target_colors_num = 4; // 目标颜色数量, 为0时, 自适应计算; 否则计算指定数目的颜色聚类
|
||||
|
||||
double smooth_weight = 0.5; // 光顺权重, 范围[0, 1], 0表示不进行光顺, 1表示完全光顺
|
||||
|
||||
// 当超采样迭代次数大于0时, 进行指定迭代次数的超采样; 否则, 自适应超采样
|
||||
std::size_t oversampling_iters = 0; // 超采样迭代次数
|
||||
std::size_t oversampling_min_face_count = 10000; // 自适应采样: 当face_count小于oversampling_min_face_count时, 进行超采样
|
||||
std::size_t oversampling_max_face_count = 1000000; // 无论输入参数如何, 超采样后的面片数不能超过oversampling_max_face_count
|
||||
|
||||
double max_color_distance = 25.0; // 自适应聚类允许的最大簇内半径(CIEDE2000 ΔE)
|
||||
std::size_t max_cluster_k = 32; // 自适应聚类的最大颜色数量上限
|
||||
|
||||
MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair;
|
||||
|
||||
// Set by TextureToColor when Ask is selected and mesh repair needs user confirmation.
|
||||
bool* mesh_repair_decision_required = nullptr;
|
||||
|
||||
MeshRepairCallback mesh_repair_callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 将纹理贴图转换为网格面片颜色, 并通过聚类和光顺生成可用于多色打印的着色网格
|
||||
*
|
||||
* 基于纹理网格的UV坐标对纹理图像进行采样, 计算每个面片的颜色,
|
||||
* 然后对颜色进行聚类(K-Means或自适应)和区域光顺, 最终输出带颜色信息的网格
|
||||
*
|
||||
* @param[in] texture_mesh 带有UV坐标的输入三角网格
|
||||
* @param[in] uv_coords 每个面片的UV坐标, 大小等于面片数, 每个面片有三个UV坐标
|
||||
* @param[in] texture 纹理图像
|
||||
* @param[out] color_mesh 输出的着色网格
|
||||
* @param[out] face_colors 输出的着色网格的面片颜色, 大小等于面片数, 颜色值为[R, G, B], 范围0~255
|
||||
* @param[in] settings 算法参数, 包括目标颜色数量、光顺权重等
|
||||
* @param[in] progress_callback 进度回调函数
|
||||
* @param[in] cancel_callback 取消回调函数
|
||||
* @return 成功返回true, 输入数据无效(空网格、无UV、空纹理等)返回false
|
||||
*/
|
||||
bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<Vec2f>>& uv_coords, const cv::Mat& texture, TriMesh& color_mesh,
|
||||
std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings = TextureToColorSettings(),
|
||||
AlgoProgressCallback progress_callback = nullptr, AlgoCancelCallback cancel_callback = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Turn pre-computed per-face colors into a clustered color mesh (no texture/UV).
|
||||
*
|
||||
* Used for OBJ vertex colors and MTL face colors, which bypass texture sampling.
|
||||
* Two routes are possible:
|
||||
* - Low-poly meshes carrying per-vertex colors: the vertex colors are quantized
|
||||
* into a small palette and the mesh is geometrically split along cluster
|
||||
* boundaries, reproducing the split topology of the legacy OBJ vertex-color
|
||||
* import. Output colors are then exact cluster centers, so mesh repair,
|
||||
* re-clustering and smoothing are skipped.
|
||||
* - Everything else: mesh repair, color clustering (K-Means or adaptive) and
|
||||
* region smoothing, sharing the same pipeline as TextureToColor.
|
||||
*
|
||||
* @param[in] mesh Input triangle mesh
|
||||
* @param[in] input_face_colors Pre-computed per-face RGB colors [0..255]
|
||||
* @param[out] out_mesh Output mesh. Geometry is subdivided on the
|
||||
* vertex-color route, and may still be replaced
|
||||
* by mesh repair on the generic route.
|
||||
* @param[out] out_face_colors Output per-face colors, one entry per out_mesh face
|
||||
* @param[in] settings Algorithm parameters (target_colors_num, smooth_weight;
|
||||
* oversampling_min_face_count doubles as the low-poly
|
||||
* threshold for the vertex-color route)
|
||||
* @param[in] progress_callback Progress callback
|
||||
* @param[in] cancel_callback Cancel callback
|
||||
* @param[in] vertex_colors Optional per-vertex RGBA [0..1]. Must match
|
||||
* mesh.vertices in size to enable the vertex-color
|
||||
* route; otherwise it is ignored.
|
||||
* @return true on success, false on failure or cancellation
|
||||
*/
|
||||
bool ClusterAndSmooth(const TriMesh& mesh,
|
||||
const std::vector<std::array<std::size_t, 3>>& input_face_colors,
|
||||
TriMesh& out_mesh,
|
||||
std::vector<std::array<std::size_t, 3>>& out_face_colors,
|
||||
const TextureToColorSettings& settings = TextureToColorSettings(),
|
||||
AlgoProgressCallback progress_callback = nullptr,
|
||||
AlgoCancelCallback cancel_callback = nullptr,
|
||||
const std::vector<std::array<float, 4>>& vertex_colors = {});
|
||||
|
||||
} // namespace tex2color
|
||||
} // namespace Slic3r
|
||||
@@ -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
|
||||
@@ -146,6 +146,85 @@ public:
|
||||
|
||||
using IntersectionLines = std::vector<IntersectionLine>;
|
||||
|
||||
// Orca: A planar face is commonly represented by multiple triangles. A slicing plane then crosses
|
||||
// their shared edges and creates intermediate 2D points which are not part of the model contour.
|
||||
// Track only edges whose two incident triangles lie in the same geometric plane within the slicing
|
||||
// coordinate precision, so those artificial junctions can be omitted without simplifying genuine,
|
||||
// nearly-collinear geometry.
|
||||
using CoplanarEdges = std::vector<bool>;
|
||||
|
||||
static CoplanarEdges coplanar_edges(const indexed_triangle_set &mesh, const std::vector<Vec3i32> &face_edge_ids,
|
||||
const Transform3d &trafo)
|
||||
{
|
||||
struct FacePlane {
|
||||
Vec3d origin { Vec3d::Zero() };
|
||||
Vec3d normal { Vec3d::Zero() };
|
||||
bool valid { false };
|
||||
};
|
||||
|
||||
// Orca: Edge IDs are dense but may include boundary edges referenced by just one face.
|
||||
int num_edges = 0;
|
||||
for (const Vec3i32 &edge_ids : face_edge_ids)
|
||||
num_edges = std::max(num_edges, edge_ids.maxCoeff() + 1);
|
||||
|
||||
CoplanarEdges coplanar(num_edges, false);
|
||||
std::vector<int> first_face(num_edges, -1);
|
||||
std::vector<int> first_face_edge(num_edges, -1);
|
||||
std::vector<FacePlane> face_planes(face_edge_ids.size());
|
||||
std::vector<bool> face_plane_computed(face_edge_ids.size(), false);
|
||||
auto transformed_vertex = [&mesh, &trafo](int vertex_idx) {
|
||||
return trafo * mesh.vertices[vertex_idx].cast<double>();
|
||||
};
|
||||
// Orca: Compute planes lazily. The single-plane slicer masks most faces, so eagerly calculating
|
||||
// every plane would defeat part of that optimization.
|
||||
auto face_plane = [&mesh, &face_planes, &face_plane_computed, &transformed_vertex](int face_idx) -> const FacePlane& {
|
||||
if (! face_plane_computed[face_idx]) {
|
||||
const Vec3i32 &face = mesh.indices[face_idx];
|
||||
const Vec3d a = transformed_vertex(face(0));
|
||||
const Vec3d b = transformed_vertex(face(1));
|
||||
const Vec3d c = transformed_vertex(face(2));
|
||||
FacePlane &plane = face_planes[face_idx];
|
||||
plane.origin = a;
|
||||
plane.normal = (b - a).cross(c - a);
|
||||
const double normal_length = plane.normal.norm();
|
||||
if (normal_length > 0.) {
|
||||
plane.normal /= normal_length;
|
||||
plane.valid = true;
|
||||
}
|
||||
face_plane_computed[face_idx] = true;
|
||||
}
|
||||
return face_planes[face_idx];
|
||||
};
|
||||
const double plane_distance_tolerance = SCALING_FACTOR;
|
||||
for (int face_idx = 0; face_idx < int(face_edge_ids.size()); ++ face_idx) {
|
||||
for (int edge_idx = 0; edge_idx < 3; ++ edge_idx) {
|
||||
const int edge_id = face_edge_ids[face_idx](edge_idx);
|
||||
if (edge_id < 0)
|
||||
continue;
|
||||
if (first_face[edge_id] == -1) {
|
||||
first_face[edge_id] = face_idx;
|
||||
first_face_edge[edge_id] = edge_idx;
|
||||
} else {
|
||||
const int first_face_idx = first_face[edge_id];
|
||||
const FacePlane &first_plane = face_plane(first_face_idx);
|
||||
const FacePlane &second_plane = face_plane(face_idx);
|
||||
const int first_opposite_idx = mesh.indices[first_face_idx]((first_face_edge[edge_id] + 2) % 3);
|
||||
const int second_opposite_idx = mesh.indices[face_idx]((edge_idx + 2) % 3);
|
||||
const Vec3d first_opposite = transformed_vertex(first_opposite_idx);
|
||||
const Vec3d second_opposite = transformed_vertex(second_opposite_idx);
|
||||
// Orca: A shared edge guarantees that the planes intersect, but not that they coincide.
|
||||
// Check both opposite vertices against the neighboring plane using one coord_t as the
|
||||
// distance tolerance. The normal dot product only preserves face orientation; it does
|
||||
// not classify a shallow angle as coplanar (see #15364).
|
||||
coplanar[edge_id] = first_plane.valid && second_plane.valid && first_plane.normal.dot(second_plane.normal) > 0. &&
|
||||
std::abs(first_plane.normal.dot(second_opposite - first_plane.origin)) <= plane_distance_tolerance &&
|
||||
std::abs(second_plane.normal.dot(first_opposite - second_plane.origin)) <= plane_distance_tolerance;
|
||||
}
|
||||
}
|
||||
}
|
||||
return coplanar;
|
||||
}
|
||||
|
||||
enum class FacetSliceType {
|
||||
NoSlice = 0,
|
||||
Slicing = 1,
|
||||
@@ -1057,7 +1136,8 @@ struct OpenPolyline {
|
||||
|
||||
// called by make_loops() to connect sliced triangles into closed loops and open polylines by the triangle connectivity.
|
||||
// Only connects segments crossing triangles of the same orientation.
|
||||
static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polygons &loops, std::vector<OpenPolyline> &open_polylines)
|
||||
static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, const CoplanarEdges &coplanar_edges,
|
||||
Polygons &loops, std::vector<OpenPolyline> &open_polylines)
|
||||
{
|
||||
// Build a map of lines by edge_a_id and a_id.
|
||||
std::vector<IntersectionLine*> by_edge_a_id;
|
||||
@@ -1134,6 +1214,11 @@ static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polyg
|
||||
(first_line->a_id != -1 && first_line->a_id == last_line->b_id)) {
|
||||
// The current loop is complete. Add it to the output.
|
||||
assert(first_line->a == last_line->b);
|
||||
// Orca: The seed point is also a triangle junction. Handle it explicitly because it
|
||||
// is never visited through the next_line branch below when the loop closes.
|
||||
if (first_line->edge_a_id >= 0 && first_line->edge_a_id < int(coplanar_edges.size()) &&
|
||||
coplanar_edges[first_line->edge_a_id])
|
||||
loop_pts.erase(loop_pts.begin());
|
||||
loops.emplace_back(std::move(loop_pts));
|
||||
#ifdef SLIC3R_TRIANGLEMESH_DEBUG
|
||||
printf(" Discovered %s polygon of %d points\n", (p.is_counter_clockwise() ? "ccw" : "cw"), (int)p.points.size());
|
||||
@@ -1153,7 +1238,12 @@ static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polyg
|
||||
next_line->a.x, next_line->a.y, next_line->b.x, next_line->b.y);
|
||||
*/
|
||||
assert(last_line->b == next_line->a);
|
||||
loop_pts.emplace_back(next_line->a);
|
||||
// Orca: Skip only junctions introduced by triangulating one planar face. Unlike a generic
|
||||
// collinearity cleanup, this preserves intentional shallow corners used when comparing
|
||||
// adjacent layers for bridges and overhang perimeters (see #15364).
|
||||
if (next_line->edge_a_id < 0 || next_line->edge_a_id >= int(coplanar_edges.size()) ||
|
||||
! coplanar_edges[next_line->edge_a_id])
|
||||
loop_pts.emplace_back(next_line->a);
|
||||
last_line = next_line;
|
||||
next_line->set_skip();
|
||||
}
|
||||
@@ -1382,7 +1472,8 @@ static void chain_open_polylines_close_gaps(std::vector<OpenPolyline> &open_poly
|
||||
|
||||
static Polygons make_loops(
|
||||
// Lines will have their flags modified.
|
||||
IntersectionLines &lines)
|
||||
IntersectionLines &lines,
|
||||
const CoplanarEdges &coplanar_edges)
|
||||
{
|
||||
Polygons loops;
|
||||
#if 0
|
||||
@@ -1412,7 +1503,7 @@ static Polygons make_loops(
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
|
||||
std::vector<OpenPolyline> open_polylines;
|
||||
chain_lines_by_triangle_connectivity(lines, loops, open_polylines);
|
||||
chain_lines_by_triangle_connectivity(lines, coplanar_edges, loops, open_polylines);
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
@@ -1484,6 +1575,7 @@ template<typename ThrowOnCancel>
|
||||
static std::vector<Polygons> make_loops(
|
||||
// Lines will have their flags modified.
|
||||
std::vector<IntersectionLines> &lines,
|
||||
const CoplanarEdges &coplanar_edges,
|
||||
const MeshSlicingParams ¶ms,
|
||||
ThrowOnCancel throw_on_cancel)
|
||||
{
|
||||
@@ -1491,20 +1583,13 @@ static std::vector<Polygons> make_loops(
|
||||
layers.resize(lines.size());
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, lines.size()),
|
||||
[&lines, &layers, ¶ms, throw_on_cancel](const tbb::blocked_range<size_t> &range) {
|
||||
[&lines, &layers, &coplanar_edges, ¶ms, throw_on_cancel](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t line_idx = range.begin(); line_idx < range.end(); ++ line_idx) {
|
||||
if ((line_idx & 0x0ffff) == 0)
|
||||
throw_on_cancel();
|
||||
|
||||
Polygons &polygons = layers[line_idx];
|
||||
polygons = make_loops(lines[line_idx]);
|
||||
|
||||
// Orca: A planar quad represented by two triangles contributes a point where the
|
||||
// slicing plane crosses the shared diagonal. After rounding to coord_t this
|
||||
// point may be very slightly off the otherwise straight contour edge. Apart
|
||||
// from being redundant, such points make the subsequent contour
|
||||
// simplification depend on the slice height (and may move seam candidates).
|
||||
remove_collinear(polygons);
|
||||
polygons = make_loops(lines[line_idx], coplanar_edges);
|
||||
|
||||
auto this_mode = line_idx < params.slicing_mode_normal_below_layer ? params.mode_below : params.mode;
|
||||
if (! polygons.empty()) {
|
||||
@@ -1633,7 +1718,7 @@ static std::vector<Polygons> make_slab_loops(
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
Polygons &loops = layers[line_idx];
|
||||
std::vector<OpenPolyline> open_polylines;
|
||||
chain_lines_by_triangle_connectivity(in, loops, open_polylines);
|
||||
chain_lines_by_triangle_connectivity(in, {}, loops, open_polylines);
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
SVG svg(debug_out_path("make_slab_loops-out-%d-%d-%s.svg", iRun, line_idx, ProjectionFromTop ? "top" : "bottom").c_str(), bbox_svg);
|
||||
@@ -1673,7 +1758,7 @@ static ExPolygons make_expolygons_simple(std::vector<IntersectionLine> &lines)
|
||||
ExPolygons slices;
|
||||
Polygons holes;
|
||||
|
||||
for (Polygon &loop : make_loops(lines))
|
||||
for (Polygon &loop : make_loops(lines, {}))
|
||||
if (loop.area() >= 0.)
|
||||
slices.emplace_back(std::move(loop));
|
||||
else
|
||||
@@ -1878,6 +1963,7 @@ std::vector<Polygons> slice_mesh(
|
||||
BOOST_LOG_TRIVIAL(debug) << "slice_mesh to polygons";
|
||||
|
||||
std::vector<IntersectionLines> lines;
|
||||
CoplanarEdges coplanar;
|
||||
|
||||
{
|
||||
//FIXME facets_edges is likely not needed and quite costly to calculate.
|
||||
@@ -1885,6 +1971,8 @@ std::vector<Polygons> slice_mesh(
|
||||
// However facets_edges assigns a single edge ID to two triangles only, thus when factoring facets_edges out, one will have
|
||||
// to make sure that no code relies on it.
|
||||
std::vector<Vec3i32> face_edge_ids = its_face_edge_ids(mesh);
|
||||
// Orca: Keep the coplanarity classification aligned with the edge IDs used to chain this slice.
|
||||
coplanar = coplanar_edges(mesh, face_edge_ids, params.trafo);
|
||||
if (zs.size() <= 1) {
|
||||
// It likely is not worthwile to copy the vertices. Apply the transformation in place.
|
||||
if (is_identity(params.trafo)) {
|
||||
@@ -1906,7 +1994,7 @@ std::vector<Polygons> slice_mesh(
|
||||
|
||||
throw_on_cancel();
|
||||
|
||||
std::vector<Polygons> layers = make_loops(lines, params, throw_on_cancel);
|
||||
std::vector<Polygons> layers = make_loops(lines, coplanar, params, throw_on_cancel);
|
||||
|
||||
#ifdef SLIC3R_DEBUG
|
||||
{
|
||||
@@ -1952,6 +2040,7 @@ Polygons slice_mesh(
|
||||
const MeshSlicingParams ¶ms)
|
||||
{
|
||||
std::vector<IntersectionLines> lines;
|
||||
CoplanarEdges coplanar;
|
||||
|
||||
{
|
||||
bool trafo_identity = is_identity(params.trafo);
|
||||
@@ -1987,6 +2076,8 @@ Polygons slice_mesh(
|
||||
|
||||
// 3) Calculate face neighbors for just the faces in face_mask.
|
||||
std::vector<Vec3i32> face_edge_ids = its_face_edge_ids(mesh, face_mask);
|
||||
// Orca: The single-plane path has its own masked edge-ID space, so classify that space separately.
|
||||
coplanar = coplanar_edges(mesh, face_edge_ids, params.trafo);
|
||||
|
||||
// 4) Slice "face_mask" triangles, collect line segments.
|
||||
// It likely is not worthwile to copy the vertices. Apply the transformation in place.
|
||||
@@ -2002,7 +2093,7 @@ Polygons slice_mesh(
|
||||
}
|
||||
|
||||
// 5) Chain the line segments.
|
||||
std::vector<Polygons> layers = make_loops(lines, params, [](){});
|
||||
std::vector<Polygons> layers = make_loops(lines, coplanar, params, [](){});
|
||||
assert(layers.size() == 1);
|
||||
return layers.front();
|
||||
}
|
||||
|
||||
@@ -1736,13 +1736,22 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
|
||||
data.used_states[n] = true;
|
||||
|
||||
if (n >= 3) {
|
||||
assert(n <= 16);
|
||||
if (n <= 16) {
|
||||
// Store "11" plus 4 bits of (n-3).
|
||||
data.bitstream.insert(data.bitstream.end(), { true, true });
|
||||
n -= 3;
|
||||
assert(n <= int(EnforcerBlockerType::ExtruderMax));
|
||||
// Store "11" plus 4 bits of (n-3), which covers states 3..17. State 18 and
|
||||
// above set that nibble to 0b1111 and store (n-18) in a second nibble. This is
|
||||
// the encoding the CONST_FILAMENTS table in Model.cpp already writes for
|
||||
// colored mesh imports.
|
||||
data.bitstream.insert(data.bitstream.end(), { true, true });
|
||||
auto &bitstream = data.bitstream;
|
||||
auto push_nibble = [&bitstream](int value) {
|
||||
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx)
|
||||
data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx));
|
||||
bitstream.push_back(value & (uint64_t(0b0001) << bit_idx));
|
||||
};
|
||||
if (n <= 17) {
|
||||
push_nibble(n - 3);
|
||||
} else {
|
||||
push_nibble(0b1111);
|
||||
push_nibble(n - 18);
|
||||
}
|
||||
} else {
|
||||
// Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams.
|
||||
@@ -1810,6 +1819,12 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
n |= data.bitstream[ibit ++] << i;
|
||||
return n;
|
||||
};
|
||||
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
|
||||
// 3..17, or 0b1111 followed by a nibble of (state-18) above that.
|
||||
auto decode_leaf_state = [&next_nibble]() {
|
||||
const int nibble = next_nibble();
|
||||
return EnforcerBlockerType(nibble == 0b1111 ? next_nibble() + 18 : nibble + 3);
|
||||
};
|
||||
|
||||
parents.clear();
|
||||
while (true) {
|
||||
@@ -1818,8 +1833,8 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
int num_of_split_sides = code & 0b11;
|
||||
int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1;
|
||||
bool is_split = num_of_children != 0;
|
||||
// Only valid if not is_split. Value of the second nibble was subtracted by 3, so it is added back.
|
||||
auto state = is_split ? EnforcerBlockerType::NONE : EnforcerBlockerType((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2);
|
||||
// Only valid if not is_split.
|
||||
auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2));
|
||||
|
||||
// BBS
|
||||
if (state == to_delete_filament)
|
||||
@@ -1916,7 +1931,14 @@ void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bi
|
||||
if (const bool is_split = (code & 0b11) != 0; is_split)
|
||||
continue;
|
||||
|
||||
const uint8_t facet_state = (code & 0b1100) == 0b1100 ? read_next_nibble() + 3 : code >> 2;
|
||||
uint8_t facet_state;
|
||||
if ((code & 0b1100) == 0b1100) {
|
||||
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
|
||||
const uint8_t nibble = read_next_nibble();
|
||||
facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3);
|
||||
} else {
|
||||
facet_state = code >> 2;
|
||||
}
|
||||
assert(facet_state < this->used_states.size());
|
||||
if (facet_state >= this->used_states.size())
|
||||
continue;
|
||||
@@ -1946,9 +1968,13 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
||||
auto num_children_or_state = [&next_nibble]() -> int {
|
||||
int code = next_nibble();
|
||||
int num_of_split_sides = code & 0b11;
|
||||
return num_of_split_sides == 0 ?
|
||||
((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) :
|
||||
- num_of_split_sides - 1;
|
||||
if (num_of_split_sides != 0)
|
||||
return - num_of_split_sides - 1;
|
||||
if ((code & 0b1100) != 0b1100)
|
||||
return code >> 2;
|
||||
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
|
||||
const int nibble = next_nibble();
|
||||
return nibble == 0b1111 ? next_nibble() + 18 : nibble + 3;
|
||||
};
|
||||
|
||||
int state = num_children_or_state();
|
||||
@@ -1983,6 +2009,20 @@ void TriangleSelector::seed_fill_unselect_all_triangles()
|
||||
triangle.unselect_by_seed_fill();
|
||||
}
|
||||
|
||||
void TriangleSelector::shift_states_above(EnforcerBlockerType threshold, int delta)
|
||||
{
|
||||
for (Triangle &triangle : m_triangles) {
|
||||
if (triangle.is_split() || !triangle.valid())
|
||||
continue;
|
||||
EnforcerBlockerType s = triangle.get_state();
|
||||
if (s >= threshold && s != EnforcerBlockerType::NONE) {
|
||||
int new_val = (int)s + delta;
|
||||
if (new_val >= 0)
|
||||
triangle.set_state(EnforcerBlockerType(new_val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TriangleSelector::seed_fill_apply_on_triangles(EnforcerBlockerType new_state)
|
||||
{
|
||||
for (Triangle &triangle : m_triangles)
|
||||
|
||||
@@ -17,7 +17,9 @@ enum class EnforcerBlockerType : int8_t {
|
||||
BLOCKER = 2,
|
||||
// For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN).
|
||||
FUZZY_SKIN = ENFORCER,
|
||||
// Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code.
|
||||
// States 3..17 are serialized into 6 bits using a 2 bit prefix code; states 18 and above use
|
||||
// one additional nibble (see TriangleSelector::serialize). ExtruderMax matches the last entry
|
||||
// of CONST_FILAMENTS in Model.cpp, which encodes the same range for colored mesh imports.
|
||||
Extruder1 = ENFORCER,
|
||||
Extruder2 = BLOCKER,
|
||||
Extruder3,
|
||||
@@ -34,7 +36,23 @@ enum class EnforcerBlockerType : int8_t {
|
||||
Extruder14,
|
||||
Extruder15,
|
||||
Extruder16,
|
||||
ExtruderMax = Extruder16
|
||||
Extruder17,
|
||||
Extruder18,
|
||||
Extruder19,
|
||||
Extruder20,
|
||||
Extruder21,
|
||||
Extruder22,
|
||||
Extruder23,
|
||||
Extruder24,
|
||||
Extruder25,
|
||||
Extruder26,
|
||||
Extruder27,
|
||||
Extruder28,
|
||||
Extruder29,
|
||||
Extruder30,
|
||||
Extruder31,
|
||||
Extruder32,
|
||||
ExtruderMax = Extruder32
|
||||
};
|
||||
|
||||
// Type alias for the state mapping array to improve code readability
|
||||
@@ -369,6 +387,9 @@ public:
|
||||
// For all triangles, remove the flag indicating that the triangle was selected by seed fill.
|
||||
void seed_fill_unselect_all_triangles();
|
||||
|
||||
// Shift all triangle states >= threshold by delta (used when inserting filaments)
|
||||
void shift_states_above(EnforcerBlockerType threshold, int delta);
|
||||
|
||||
// For all triangles selected by seed fill, set new EnforcerBlockerType and remove flag indicating that triangle was selected by seed fill.
|
||||
// The operation may merge split triangles if they are being assigned the same color.
|
||||
void seed_fill_apply_on_triangles(EnforcerBlockerType new_state);
|
||||
|
||||
@@ -64,6 +64,11 @@ static constexpr double LARGE_BED_THRESHOLD = 2147;
|
||||
// Orca: maximum number of extruders is 64. For SEMM printers, it defines maximum filament number.
|
||||
static constexpr size_t MAXIMUM_EXTRUDER_NUMBER = 64;
|
||||
|
||||
// Orca: how many filament slots syncing an AMS setup may create. This was derived from
|
||||
// EnforcerBlockerType::ExtruderMax, but that cap now covers 32 paintable filaments, so the AMS
|
||||
// limit is pinned here to keep sync behaving as it does for projects without mixed-color filaments.
|
||||
static constexpr size_t MAXIMUM_AMS_SYNC_FILAMENT_NUMBER = 16;
|
||||
|
||||
// Orca: maximum line width is 5 times the nozzle diameter
|
||||
static constexpr float MAX_LINE_WIDTH_MULTIPLIER = 5;
|
||||
|
||||
|
||||
@@ -355,6 +355,16 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Monitor.hpp
|
||||
GUI/MonitorPage.cpp
|
||||
GUI/MonitorPage.hpp
|
||||
GUI/MixedFilamentDialog.cpp
|
||||
GUI/MixedFilamentDialog.hpp
|
||||
GUI/GradientCurveEditor.cpp
|
||||
GUI/GradientCurveEditor.hpp
|
||||
GUI/ColorDecomposeDialog.cpp
|
||||
GUI/ColorDecomposeDialog.hpp
|
||||
GUI/ColorDecomposeSupport.cpp
|
||||
GUI/ColorDecomposeSupport.hpp
|
||||
GUI/TextureImportDialog.cpp
|
||||
GUI/TextureImportDialog.hpp
|
||||
GUI/Mouse3DController.cpp
|
||||
GUI/Mouse3DController.hpp
|
||||
GUI/MsgDialog.cpp
|
||||
|
||||
@@ -682,13 +682,19 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
if (shader) {
|
||||
if (idx == 0) {
|
||||
int extruder_id = model_volume->extruder_id();
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - (extruder_id - 1))/255.0f;
|
||||
// ORCA: extruder_id may be 0 (unset) or point past the colour list after a
|
||||
// filament is deleted/remapped, so clamp the index instead of reading out of
|
||||
// bounds.
|
||||
if (!extruder_colors.empty()) {
|
||||
int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1);
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - color_idx)/255.0f;
|
||||
}
|
||||
m.set_color(new_color);
|
||||
// shader->set_uniform("uniform_color", new_color);
|
||||
}
|
||||
m.set_color(new_color);
|
||||
// shader->set_uniform("uniform_color", new_color);
|
||||
}
|
||||
else {
|
||||
if (idx <= extruder_colors.size()) {
|
||||
|
||||
@@ -193,7 +193,7 @@ public:
|
||||
|
||||
void show_panels(CalibrationMethod method, const PrinterSeries printer_ser);
|
||||
|
||||
void on_device_connected(MachineObject* obj);
|
||||
void on_device_connected(MachineObject* obj) override;
|
||||
|
||||
void update(MachineObject* obj) override;
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ public:
|
||||
|
||||
void create_page(wxWindow* parent);
|
||||
|
||||
void on_reset_page();
|
||||
void on_device_connected(MachineObject* obj);
|
||||
void on_reset_page() override;
|
||||
void on_device_connected(MachineObject* obj) override;
|
||||
void msw_rescale() override;
|
||||
};
|
||||
|
||||
@@ -63,8 +63,8 @@ public:
|
||||
long style = wxTAB_TRAVERSAL);
|
||||
|
||||
void create_page(wxWindow* parent);
|
||||
void on_reset_page();
|
||||
void on_device_connected(MachineObject* obj);
|
||||
void on_reset_page() override;
|
||||
void on_device_connected(MachineObject* obj) override;
|
||||
void msw_rescale() override;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include "wx/graphics.h"
|
||||
|
||||
#include "I18N.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "format.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "Widgets/DropDown.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "libslic3r/ColorDecomposeRecipe.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static const wxColour COLOR_BRAND("#009688");
|
||||
static const wxColour COLOR_BORDER_NORMAL("#EEEEEE");
|
||||
static const wxColour COLOR_BG_CARD("#F8F8F8");
|
||||
static const wxColour COLOR_LABEL_GREY("#ACACAC");
|
||||
static const wxColour COLOR_TEXT_DARK("#262E30");
|
||||
static const wxColour COLOR_DIVIDER("#EEEEEE");
|
||||
|
||||
// Standard CMYW base colors
|
||||
static const wxColour CMYW_CYAN(0, 255, 255);
|
||||
static const wxColour CMYW_MAGENTA(255, 0, 255);
|
||||
static const wxColour CMYW_YELLOW(255, 255, 0);
|
||||
static const wxColour CMYW_WHITE(255, 255, 255);
|
||||
|
||||
// Standard RYBW base colors
|
||||
static const wxColour RYBW_RED(255, 0, 0);
|
||||
static const wxColour RYBW_YELLOW(255, 255, 0);
|
||||
static const wxColour RYBW_BLUE(0, 0, 255);
|
||||
static const wxColour RYBW_WHITE(255, 255, 255);
|
||||
|
||||
static size_t mode_index(DecomposeMode mode)
|
||||
{
|
||||
return static_cast<size_t>(mode);
|
||||
}
|
||||
|
||||
static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color)
|
||||
{
|
||||
return {
|
||||
static_cast<unsigned char>(color.Red()),
|
||||
static_cast<unsigned char>(color.Green()),
|
||||
static_cast<unsigned char>(color.Blue())
|
||||
};
|
||||
}
|
||||
|
||||
static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback)
|
||||
{
|
||||
wxColour color(hex);
|
||||
return color.IsOk() ? color : fallback;
|
||||
}
|
||||
|
||||
static bool same_rgb(const wxColour& lhs, const wxColour& rhs)
|
||||
{
|
||||
return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue();
|
||||
}
|
||||
|
||||
static DecomposeBaseColor standard_base_color_from_key(const std::string& key)
|
||||
{
|
||||
if (key == "Cyan") return DecomposeBaseColor::Cyan;
|
||||
if (key == "Magenta") return DecomposeBaseColor::Magenta;
|
||||
if (key == "Yellow") return DecomposeBaseColor::Yellow;
|
||||
if (key == "White") return DecomposeBaseColor::White;
|
||||
if (key == "Red") return DecomposeBaseColor::Red;
|
||||
if (key == "Green") return DecomposeBaseColor::Green;
|
||||
if (key == "Blue") return DecomposeBaseColor::Blue;
|
||||
return DecomposeBaseColor::None;
|
||||
}
|
||||
|
||||
static wxColour pure_color_for_base(DecomposeBaseColor base)
|
||||
{
|
||||
switch (base) {
|
||||
case DecomposeBaseColor::Cyan: return CMYW_CYAN;
|
||||
case DecomposeBaseColor::Magenta: return CMYW_MAGENTA;
|
||||
case DecomposeBaseColor::Yellow: return CMYW_YELLOW;
|
||||
case DecomposeBaseColor::White: return CMYW_WHITE;
|
||||
case DecomposeBaseColor::Red: return RYBW_RED;
|
||||
case DecomposeBaseColor::Blue: return RYBW_BLUE;
|
||||
default: return *wxBLACK;
|
||||
}
|
||||
}
|
||||
|
||||
static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color)
|
||||
{
|
||||
if (mode == DecomposeMode::CMYW) {
|
||||
if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan;
|
||||
if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta;
|
||||
if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow;
|
||||
if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White;
|
||||
} else if (mode == DecomposeMode::RYBW) {
|
||||
if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red;
|
||||
if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow;
|
||||
if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue;
|
||||
if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White;
|
||||
}
|
||||
return DecomposeBaseColor::None;
|
||||
}
|
||||
|
||||
static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe,
|
||||
const wxColour& fallback)
|
||||
{
|
||||
ColorDecomposeResult result;
|
||||
result.mode = recipe.mode;
|
||||
result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback);
|
||||
for (const auto& comp_recipe : recipe.components) {
|
||||
DecomposeComponent comp;
|
||||
comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback);
|
||||
comp.ratio = comp_recipe.ratio;
|
||||
comp.filament_index = static_cast<int>(comp_recipe.filament_index);
|
||||
comp.base_color = standard_base_color_from_key(comp_recipe.base_color);
|
||||
if (comp.base_color == DecomposeBaseColor::None)
|
||||
comp.base_color = standard_base_color_for(recipe.mode, comp.colour);
|
||||
result.components.push_back(comp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1)
|
||||
{
|
||||
const int h = parent->FromDIP(1);
|
||||
int w = fixed_width > 0 ? fixed_width : -1;
|
||||
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h));
|
||||
panel->SetMinSize(wxSize(w, h));
|
||||
if (fixed_width > 0)
|
||||
panel->SetMaxSize(wxSize(fixed_width, h));
|
||||
panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER));
|
||||
return panel;
|
||||
}
|
||||
|
||||
static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text)
|
||||
{
|
||||
auto* label = new wxStaticText(parent, wxID_ANY, text);
|
||||
label->SetFont(Label::Body_11);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY));
|
||||
return label;
|
||||
}
|
||||
|
||||
static void match_parent_bg(wxWindow* w, const wxColour& bg)
|
||||
{
|
||||
w->SetBackgroundColour(bg);
|
||||
}
|
||||
|
||||
static bool material_type_matches(const std::string& a, const std::string& b)
|
||||
{
|
||||
if (a.empty() || b.empty())
|
||||
return false;
|
||||
return a == b || a == b + " Basic" || b == a + " Basic";
|
||||
}
|
||||
|
||||
|
||||
ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent,
|
||||
int filament_idx,
|
||||
const wxColour& target_color,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& filament_names,
|
||||
const std::vector<std::string>& filament_types,
|
||||
size_t current_filament_count,
|
||||
size_t max_filament_count,
|
||||
std::vector<size_t> physical_config_indices)
|
||||
: DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition,
|
||||
wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
, m_filament_idx(filament_idx)
|
||||
, m_target_color(target_color)
|
||||
, m_physical_colors(physical_colors)
|
||||
, m_filament_names(filament_names)
|
||||
, m_filament_types(filament_types)
|
||||
, m_current_filament_count(current_filament_count)
|
||||
, m_max_filament_count(max_filament_count)
|
||||
, m_physical_config_indices(std::move(physical_config_indices))
|
||||
{
|
||||
for (const auto& t : m_filament_types) {
|
||||
if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end())
|
||||
m_project_types.push_back(t);
|
||||
}
|
||||
|
||||
if (m_filament_idx >= 0 && static_cast<size_t>(m_filament_idx) < m_filament_types.size())
|
||||
m_preferred_type = m_filament_types[m_filament_idx];
|
||||
else if (!m_project_types.empty())
|
||||
m_preferred_type = m_project_types.front();
|
||||
|
||||
build_ui();
|
||||
wxGetApp().UpdateDlgDarkUI(this);
|
||||
// Restore target swatch after dark mode color remapping
|
||||
if (m_target_swatch) {
|
||||
m_target_swatch->SetBackgroundColour(m_target_color);
|
||||
m_target_swatch->Refresh();
|
||||
}
|
||||
|
||||
update_card_visibility();
|
||||
Fit();
|
||||
compute_decomposition();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
{
|
||||
(void)suggested_rect;
|
||||
Fit();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::build_ui()
|
||||
{
|
||||
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
|
||||
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
const int selector_side_margin = FromDIP(26);
|
||||
const int selector_top_gap = FromDIP(22);
|
||||
const int content_side_margin = FromDIP(30);
|
||||
const int target_section_top_gap = FromDIP(18);
|
||||
|
||||
main_sizer->AddSpacer(selector_top_gap);
|
||||
main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin);
|
||||
main_sizer->AddSpacer(target_section_top_gap);
|
||||
main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin);
|
||||
|
||||
SetSizer(main_sizer);
|
||||
SetMinSize(wxSize(FromDIP(477), FromDIP(380)));
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_filament_selector()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
|
||||
wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY);
|
||||
m_type_combo->SetFont(Label::Body_13);
|
||||
|
||||
m_combo_item_types.clear();
|
||||
int default_sel = -1;
|
||||
|
||||
// --- Group 1: Project filament list (deduplicated by type) ---
|
||||
m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
|
||||
m_combo_item_types.push_back(std::string());
|
||||
|
||||
std::set<std::string> seen_types;
|
||||
for (size_t i = 0; i < m_filament_names.size(); ++i) {
|
||||
const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA";
|
||||
if (!seen_types.insert(type).second)
|
||||
continue;
|
||||
int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i]));
|
||||
m_combo_item_types.push_back(type);
|
||||
if (type == m_preferred_type && default_sel < 0)
|
||||
default_sel = idx;
|
||||
}
|
||||
|
||||
// --- Group 2: Standard mode material recommendations ---
|
||||
static const char* kStandardTypes[] = {
|
||||
kDecomposePlaBasicType
|
||||
};
|
||||
|
||||
m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
|
||||
m_combo_item_types.push_back(std::string());
|
||||
|
||||
for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) {
|
||||
// Always show standard recommendations, even if the same type already
|
||||
// appears in the project filament list above.
|
||||
const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s];
|
||||
int idx = m_type_combo->Append(wxString::FromUTF8(label));
|
||||
m_combo_item_types.push_back(kStandardTypes[s]);
|
||||
if (kStandardTypes[s] == m_preferred_type && default_sel < 0)
|
||||
default_sel = idx;
|
||||
}
|
||||
|
||||
if (default_sel < 0) {
|
||||
for (int i = 0; i < static_cast<int>(m_combo_item_types.size()); ++i) {
|
||||
if (!m_combo_item_types[i].empty()) {
|
||||
default_sel = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (default_sel >= 0) {
|
||||
m_type_combo->SetSelection(default_sel);
|
||||
if (!m_combo_item_types[default_sel].empty())
|
||||
m_preferred_type = m_combo_item_types[default_sel];
|
||||
}
|
||||
|
||||
m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) {
|
||||
evt.StopPropagation();
|
||||
int sel = m_type_combo->GetSelection();
|
||||
if (sel >= 0 && static_cast<size_t>(sel) < m_combo_item_types.size()
|
||||
&& !m_combo_item_types[sel].empty()) {
|
||||
m_preferred_type = m_combo_item_types[sel];
|
||||
}
|
||||
update_card_visibility();
|
||||
compute_decomposition();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
});
|
||||
|
||||
sizer->Add(m_type_combo, 1, wxEXPAND);
|
||||
return sizer;
|
||||
}
|
||||
|
||||
static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size)
|
||||
{
|
||||
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size));
|
||||
panel->SetBackgroundColour(color);
|
||||
panel->SetMinSize(wxSize(size, size));
|
||||
panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
panel->Bind(wxEVT_PAINT, [panel](wxPaintEvent&) {
|
||||
wxAutoBufferedPaintDC dc(panel);
|
||||
wxSize sz = panel->GetClientSize();
|
||||
wxColour c = panel->GetBackgroundColour();
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(c));
|
||||
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
|
||||
// Mirror sidebar (FilamentBitmapUtils::create_single_filament_bitmap):
|
||||
// gray border for near-white in light mode so white swatches stay
|
||||
// visible on a white background; light border for near-black in dark mode.
|
||||
const bool light_mode = !wxGetApp().dark_mode();
|
||||
if ((light_mode && c.Red() > 224 && c.Green() > 224 && c.Blue() > 224) ||
|
||||
(!light_mode && c.Red() < 45 && c.Green() < 45 && c.Blue() < 45)) {
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
dc.SetPen(wxPen(light_mode ? wxColour(130, 130, 128) : wxColour(207, 207, 207),
|
||||
1, wxPENSTYLE_SOLID));
|
||||
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
|
||||
}
|
||||
});
|
||||
return panel;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_target_color_section()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color"));
|
||||
label->SetFont(Label::Head_14);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19));
|
||||
|
||||
m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
|
||||
sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_target_rgb_text = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
|
||||
m_target_rgb_text->SetFont(Label::Body_13);
|
||||
m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
|
||||
arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
|
||||
sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_matched_rgb_text = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
|
||||
m_matched_rgb_text->SetFont(Label::Head_13);
|
||||
m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode,
|
||||
const wxString& title)
|
||||
{
|
||||
const int pad = FromDIP(12);
|
||||
|
||||
auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
card->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
auto* card_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* title_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* title_label = new wxStaticText(card, wxID_ANY, title);
|
||||
title_label->SetFont(Label::Body_14);
|
||||
title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
|
||||
match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
auto* chk = new ::CheckBox(card);
|
||||
chk->SetValue(mode == m_selected_mode);
|
||||
match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
switch (mode) {
|
||||
case DecomposeMode::MaterialList: m_chk_material_list = chk; break;
|
||||
case DecomposeMode::CMYW: m_chk_cmyw = chk; break;
|
||||
case DecomposeMode::RYBW: m_chk_rybw = chk; break;
|
||||
}
|
||||
chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) {
|
||||
select_mode(mode);
|
||||
e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue()
|
||||
});
|
||||
title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad);
|
||||
|
||||
card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8));
|
||||
|
||||
auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
|
||||
|
||||
auto& controls = m_mode_cards[mode_index(mode)];
|
||||
controls.card = card;
|
||||
controls.components_sizer = colors_sizer;
|
||||
|
||||
card->SetSizer(card_sizer);
|
||||
card->SetMinSize(wxSize(FromDIP(128), FromDIP(111)));
|
||||
card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111)));
|
||||
|
||||
card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) {
|
||||
wxBufferedPaintDC dc(card);
|
||||
wxSize sz = card->GetClientSize();
|
||||
dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE)));
|
||||
dc.Clear();
|
||||
|
||||
bool selected = (m_selected_mode == mode);
|
||||
wxColour border_col = selected
|
||||
? StateColor::darkModeColorFor(COLOR_BRAND)
|
||||
: StateColor::darkModeColorFor(COLOR_BORDER_NORMAL);
|
||||
const int border_width = FromDIP(selected ? 2 : 1);
|
||||
const double inset = border_width / 2.0;
|
||||
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
|
||||
if (gc) {
|
||||
gc->SetPen(wxPen(border_col, border_width));
|
||||
gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
|
||||
gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8));
|
||||
} else {
|
||||
const int fallback_inset = (border_width + 1) / 2;
|
||||
dc.SetPen(wxPen(border_col, border_width));
|
||||
dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
|
||||
dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8));
|
||||
}
|
||||
});
|
||||
|
||||
std::function<void(wxWindow*)> bind_click;
|
||||
bind_click = [this, mode, chk, &bind_click](wxWindow* w) {
|
||||
if (w == chk || dynamic_cast<::CheckBox*>(w))
|
||||
return;
|
||||
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
|
||||
select_mode(mode);
|
||||
});
|
||||
w->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
for (auto* child : w->GetChildren())
|
||||
bind_click(child);
|
||||
};
|
||||
bind_click(card);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition"));
|
||||
section_label->SetFont(Label::Head_14);
|
||||
section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4));
|
||||
|
||||
auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// --- Arbitrary mode column (wrapped in a panel so the whole column hides together) ---
|
||||
m_arb_column_panel = new wxPanel(this, wxID_ANY);
|
||||
m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
auto* arb_col = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL);
|
||||
arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
||||
|
||||
m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList,
|
||||
_L("Material List"));
|
||||
arb_col->Add(m_card_material_list, 0, wxEXPAND);
|
||||
}
|
||||
m_arb_column_panel->SetSizer(arb_col);
|
||||
modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16));
|
||||
|
||||
// --- Standard mode column ---
|
||||
auto* std_col = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL);
|
||||
std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
||||
|
||||
auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW");
|
||||
cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12));
|
||||
|
||||
m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW");
|
||||
cards_sizer->Add(m_card_rybw, 0);
|
||||
|
||||
std_col->Add(cards_sizer, 0, wxEXPAND);
|
||||
}
|
||||
modes_sizer->Add(std_col, 0, wxEXPAND);
|
||||
|
||||
sizer->Add(modes_sizer, 0, wxEXPAND);
|
||||
|
||||
m_no_card_hint = new wxStaticText(this, wxID_ANY,
|
||||
_L("At least two filaments of the same material type are required for decomposition"));
|
||||
m_no_card_hint->SetFont(Label::Body_13);
|
||||
m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
|
||||
m_no_card_hint->Wrap(FromDIP(400));
|
||||
m_no_card_hint->Hide();
|
||||
sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8));
|
||||
|
||||
m_limit_warning_panel = new wxPanel(this, wxID_ANY);
|
||||
m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY,
|
||||
create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16),
|
||||
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
|
||||
m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString);
|
||||
m_limit_warning_text->SetFont(Label::Body_13);
|
||||
m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B")));
|
||||
m_limit_warning_text->Wrap(FromDIP(400));
|
||||
warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6));
|
||||
warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND);
|
||||
m_limit_warning_panel->SetSizer(warning_sizer);
|
||||
m_limit_warning_panel->Hide();
|
||||
sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8));
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_button_panel()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer->AddStretchSpacer();
|
||||
|
||||
m_btn_cancel = new Button(this, _L("Cancel"));
|
||||
m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
|
||||
m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
|
||||
m_btn_ok = new Button(this, _L("OK"));
|
||||
m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
|
||||
m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
EndModal(wxID_OK);
|
||||
});
|
||||
|
||||
sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12));
|
||||
sizer->Add(m_btn_ok, 0);
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::select_mode(DecomposeMode mode)
|
||||
{
|
||||
m_selected_mode = mode;
|
||||
m_result = m_mode_results[mode_index(mode)];
|
||||
update_card_styles();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_card_styles()
|
||||
{
|
||||
if (m_card_material_list) m_card_material_list->Refresh();
|
||||
if (m_card_cmyw) m_card_cmyw->Refresh();
|
||||
if (m_card_rybw) m_card_rybw->Refresh();
|
||||
|
||||
if (m_chk_material_list)
|
||||
m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList);
|
||||
if (m_chk_cmyw)
|
||||
m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW);
|
||||
if (m_chk_rybw)
|
||||
m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW);
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_card_visibility()
|
||||
{
|
||||
// Count physical filaments of the same type (excluding the source filament)
|
||||
int same_type_count = 0;
|
||||
for (size_t i = 0; i < m_filament_types.size(); ++i) {
|
||||
if (static_cast<int>(i) == m_filament_idx)
|
||||
continue;
|
||||
if (material_type_matches(m_filament_types[i], m_preferred_type))
|
||||
++same_type_count;
|
||||
}
|
||||
|
||||
bool show_arb = (same_type_count >= 2);
|
||||
bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType);
|
||||
bool show_rybw = (m_preferred_type == kDecomposePlaBasicType);
|
||||
|
||||
if (m_arb_column_panel) m_arb_column_panel->Show(show_arb);
|
||||
if (m_card_material_list) m_card_material_list->Show(show_arb);
|
||||
if (m_card_cmyw) m_card_cmyw->Show(show_cmyw);
|
||||
if (m_card_rybw) m_card_rybw->Show(show_rybw);
|
||||
|
||||
bool any_visible = show_arb || show_cmyw || show_rybw;
|
||||
if (m_no_card_hint)
|
||||
m_no_card_hint->Show(!any_visible);
|
||||
|
||||
// Auto-select a visible mode when current selection becomes hidden
|
||||
if (any_visible) {
|
||||
bool cur_visible = false;
|
||||
if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true;
|
||||
if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true;
|
||||
if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true;
|
||||
if (!cur_visible) {
|
||||
if (show_arb) select_mode(DecomposeMode::MaterialList);
|
||||
else if (show_cmyw) select_mode(DecomposeMode::CMYW);
|
||||
else select_mode(DecomposeMode::RYBW);
|
||||
}
|
||||
}
|
||||
|
||||
Layout();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
{
|
||||
if (!m_limit_warning_panel || !m_limit_warning_text)
|
||||
return;
|
||||
|
||||
size_t missing_new = 0;
|
||||
if (m_missing_calculator) {
|
||||
missing_new = m_missing_calculator(m_result);
|
||||
} else {
|
||||
const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast<size_t>(m_filament_idx) : size_t(-1);
|
||||
const std::vector<size_t>* indices =
|
||||
m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices;
|
||||
missing_new = count_decompose_new_physical_filaments(
|
||||
m_result, m_physical_colors, m_filament_types, source_physical_idx, indices);
|
||||
}
|
||||
// A result with fewer than 2 components (e.g. target color is already a
|
||||
// standard base color shown as "100%") creates no mixed filament and no new
|
||||
// physical filament, so it can never exceed the limit.
|
||||
const bool creates_mixed = m_result.components.size() >= 2;
|
||||
// +1 for the mixed filament slot that will be created after decomposition.
|
||||
const size_t needed = m_current_filament_count + missing_new + 1;
|
||||
const bool blocked = creates_mixed && needed > m_max_filament_count;
|
||||
|
||||
const bool was_shown = m_limit_warning_panel->IsShown();
|
||||
|
||||
if (!blocked) {
|
||||
if (was_shown) {
|
||||
m_limit_warning_panel->Hide();
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wxString mode_name;
|
||||
switch (m_selected_mode) {
|
||||
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
|
||||
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
|
||||
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
|
||||
}
|
||||
|
||||
const wxString warning_text = format_wxstr(
|
||||
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
|
||||
m_max_filament_count, mode_name);
|
||||
|
||||
// Show first so the panel is laid out and the text control gets its real
|
||||
// width, then wrap to that width so the paragraph fills the content area.
|
||||
m_limit_warning_panel->Show();
|
||||
Layout();
|
||||
const int avail = m_limit_warning_text->GetClientSize().x;
|
||||
m_limit_warning_text->SetLabel(warning_text);
|
||||
if (avail > FromDIP(50))
|
||||
m_limit_warning_text->Wrap(avail);
|
||||
|
||||
Layout();
|
||||
// Only resize when the warning panel actually toggled from hidden to shown.
|
||||
// While already visible, switching modes must not re-Fit the dialog, which
|
||||
// would make it jump on every card switch. Fit keeps the user-moved position.
|
||||
if (!was_shown) {
|
||||
Fit();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn)
|
||||
{
|
||||
m_missing_calculator = std::move(fn);
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_ok_button_state()
|
||||
{
|
||||
if (!m_btn_ok) return;
|
||||
update_filament_limit_warning();
|
||||
bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown())
|
||||
|| (m_card_cmyw && m_card_cmyw->IsShown())
|
||||
|| (m_card_rybw && m_card_rybw->IsShown());
|
||||
const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown();
|
||||
m_btn_ok->Enable(any_card_visible && !blocked);
|
||||
Layout();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode)
|
||||
{
|
||||
auto& controls = m_mode_cards[mode_index(mode)];
|
||||
auto* sizer = controls.components_sizer;
|
||||
auto* card = controls.card;
|
||||
if (!sizer || !card)
|
||||
return;
|
||||
|
||||
sizer->Clear(true);
|
||||
const auto& components = m_mode_results[mode_index(mode)].components;
|
||||
const size_t count = components.size();
|
||||
if (count == 0) {
|
||||
card->Layout();
|
||||
card->Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const int swatch_sz = FromDIP(24);
|
||||
const int plus_gap = FromDIP(24);
|
||||
const wxFont& ratio_font = Label::Body_13;
|
||||
auto bind_select = [this, mode](wxWindow* w) {
|
||||
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
|
||||
select_mode(mode);
|
||||
});
|
||||
w->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto* col = new wxBoxSizer(wxVERTICAL);
|
||||
auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz);
|
||||
bind_select(swatch);
|
||||
col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL);
|
||||
auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio));
|
||||
ratio_text->SetFont(ratio_font);
|
||||
ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
bind_select(ratio_text);
|
||||
col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4));
|
||||
sizer->Add(col, 0, wxALIGN_TOP);
|
||||
|
||||
if (i + 1 < count) {
|
||||
sizer->AddStretchSpacer();
|
||||
auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
auto* plus_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+");
|
||||
plus_label->SetFont(Label::Body_13);
|
||||
plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
bind_select(plus_panel);
|
||||
bind_select(plus_label);
|
||||
plus_sizer->AddStretchSpacer();
|
||||
plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL);
|
||||
plus_sizer->AddStretchSpacer();
|
||||
plus_panel->SetSizer(plus_sizer);
|
||||
sizer->Add(plus_panel, 0, wxALIGN_TOP);
|
||||
sizer->AddStretchSpacer();
|
||||
}
|
||||
}
|
||||
|
||||
const int card_width = FromDIP(128 + (count > 2 ? static_cast<int>(count - 2) * 31 : 0));
|
||||
card->SetMinSize(wxSize(card_width, FromDIP(111)));
|
||||
card->SetMaxSize(wxSize(card_width, FromDIP(111)));
|
||||
|
||||
card->Layout();
|
||||
card->Refresh();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_mode_card_contents()
|
||||
{
|
||||
update_mode_card_content(DecomposeMode::MaterialList);
|
||||
update_mode_card_content(DecomposeMode::CMYW);
|
||||
update_mode_card_content(DecomposeMode::RYBW);
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_matched_color_display()
|
||||
{
|
||||
if (!m_result.matched_color.IsOk())
|
||||
m_result.matched_color = m_target_color;
|
||||
|
||||
if (m_matched_swatch) {
|
||||
m_matched_swatch->SetBackgroundColour(m_result.matched_color);
|
||||
m_matched_swatch->Refresh();
|
||||
}
|
||||
if (m_matched_rgb_text) {
|
||||
m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d",
|
||||
m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue()));
|
||||
}
|
||||
}
|
||||
|
||||
bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const
|
||||
{
|
||||
// Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic.
|
||||
if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) {
|
||||
if (m_preferred_type != kDecomposePlaBasicType)
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const DecomposeBaseColor cmyw_bases[] = {
|
||||
DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta,
|
||||
DecomposeBaseColor::Yellow, DecomposeBaseColor::White
|
||||
};
|
||||
static const DecomposeBaseColor rybw_bases[] = {
|
||||
DecomposeBaseColor::Red, DecomposeBaseColor::Yellow,
|
||||
DecomposeBaseColor::Blue, DecomposeBaseColor::White
|
||||
};
|
||||
const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases;
|
||||
const size_t base_count = (mode == DecomposeMode::CMYW)
|
||||
? sizeof(cmyw_bases) / sizeof(cmyw_bases[0])
|
||||
: sizeof(rybw_bases) / sizeof(rybw_bases[0]);
|
||||
|
||||
const std::string target_hex = decompose_normalize_color_hex(
|
||||
m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
|
||||
for (size_t i = 0; i < base_count; ++i) {
|
||||
const DecomposeBaseColor base = bases[i];
|
||||
DecomposeOfficialComponent official =
|
||||
lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base));
|
||||
if (decompose_normalize_color_hex(official.color_hex) != target_hex)
|
||||
continue;
|
||||
|
||||
out = ColorDecomposeResult{};
|
||||
out.mode = mode;
|
||||
out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color);
|
||||
DecomposeComponent comp;
|
||||
comp.colour = out.matched_color;
|
||||
comp.ratio = 100;
|
||||
comp.filament_index = -1;
|
||||
comp.base_color = base;
|
||||
out.components.push_back(comp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::compute_decomposition()
|
||||
{
|
||||
auto fallback_result = [this](DecomposeMode mode, const std::vector<DecomposeComponent>& components) {
|
||||
ColorDecomposeResult result;
|
||||
result.mode = mode;
|
||||
result.components = components;
|
||||
int total = 0;
|
||||
double r = 0.0, g = 0.0, b = 0.0;
|
||||
for (const auto& comp : result.components)
|
||||
total += comp.ratio;
|
||||
if (total <= 0)
|
||||
total = 100;
|
||||
for (const auto& comp : result.components) {
|
||||
const double w = static_cast<double>(comp.ratio) / total;
|
||||
r += comp.colour.Red() * w;
|
||||
g += comp.colour.Green() * w;
|
||||
b += comp.colour.Blue() * w;
|
||||
}
|
||||
result.matched_color = result.components.empty()
|
||||
? m_target_color
|
||||
: wxColour(static_cast<unsigned char>(std::clamp(r, 0.0, 255.0)),
|
||||
static_cast<unsigned char>(std::clamp(g, 0.0, 255.0)),
|
||||
static_cast<unsigned char>(std::clamp(b, 0.0, 255.0)));
|
||||
return result;
|
||||
};
|
||||
|
||||
std::vector<ColorDecomposePhysicalFilament> physical_filaments;
|
||||
physical_filaments.reserve(m_physical_colors.size());
|
||||
for (size_t i = 0; i < m_physical_colors.size(); ++i) {
|
||||
if (m_filament_idx >= 0 && i == static_cast<size_t>(m_filament_idx))
|
||||
continue;
|
||||
ColorDecomposePhysicalFilament filament;
|
||||
filament.color_hex = m_physical_colors[i];
|
||||
filament.name = i < m_filament_names.size() ? m_filament_names[i] : "";
|
||||
filament.type = i < m_filament_types.size() ? m_filament_types[i] : "";
|
||||
filament.filament_index = static_cast<unsigned int>(i + 1);
|
||||
physical_filaments.push_back(std::move(filament));
|
||||
}
|
||||
|
||||
const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color);
|
||||
|
||||
auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type);
|
||||
if (material_recipe.valid) {
|
||||
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
|
||||
to_dialog_result(material_recipe, m_target_color);
|
||||
} else {
|
||||
std::vector<DecomposeComponent> components;
|
||||
for (size_t i = 0; i < std::min<size_t>(2, physical_filaments.size()); ++i) {
|
||||
DecomposeComponent comp;
|
||||
comp.colour = wxColour(physical_filaments[i].color_hex);
|
||||
comp.ratio = 50;
|
||||
comp.filament_index = static_cast<int>(physical_filaments[i].filament_index);
|
||||
components.push_back(comp);
|
||||
}
|
||||
if (components.empty()) {
|
||||
components.push_back({m_target_color, 100, -1});
|
||||
} else if (components.size() == 1) {
|
||||
components.front().ratio = 100;
|
||||
}
|
||||
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
|
||||
fallback_result(DecomposeMode::MaterialList, components);
|
||||
}
|
||||
|
||||
ColorDecomposeResult single_base;
|
||||
if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) {
|
||||
m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base;
|
||||
} else {
|
||||
auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type);
|
||||
m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid
|
||||
? to_dialog_result(cmyw_recipe, m_target_color)
|
||||
: fallback_result(DecomposeMode::CMYW, {
|
||||
{CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
|
||||
{CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan}
|
||||
});
|
||||
}
|
||||
|
||||
if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) {
|
||||
m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base;
|
||||
} else {
|
||||
auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type);
|
||||
m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid
|
||||
? to_dialog_result(rybw_recipe, m_target_color)
|
||||
: fallback_result(DecomposeMode::RYBW, {
|
||||
{RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
|
||||
{RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue}
|
||||
});
|
||||
}
|
||||
|
||||
m_result = m_mode_results[mode_index(m_selected_mode)];
|
||||
update_mode_card_contents();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -577,22 +577,67 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
|
||||
}
|
||||
|
||||
// BBS
|
||||
static const char* keys[] = { "support_filament", "support_interface_filament"};
|
||||
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
||||
std::string key = std::string(keys[i]);
|
||||
// Reset filament overrides pointing at a slot that no longer exists. Support and the wipe
|
||||
// tower additionally reject mixed slots: the engine consumes those keys directly, so a virtual
|
||||
// slot would reach the G-code unresolved, while the per-feature keys are resolved per layer.
|
||||
static const char* physical_only_keys[] = { "support_filament", "support_interface_filament", "wipe_tower_filament" };
|
||||
static const char* feature_keys[] = { "outer_wall_filament_id", "inner_wall_filament_id",
|
||||
"sparse_infill_filament_id", "internal_solid_filament_id",
|
||||
"top_surface_filament_id", "bottom_surface_filament_id" };
|
||||
auto reset_invalid_filament = [this, config, filament_cnt](const char* key, bool allow_mixed) {
|
||||
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
|
||||
if (opt != nullptr) {
|
||||
if (opt->getInt() > filament_cnt) {
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
|
||||
int new_value = 0;
|
||||
if (conf_temp != nullptr && conf_temp->has(key)) {
|
||||
new_value = conf_temp->opt_int(key);
|
||||
if (opt == nullptr)
|
||||
return;
|
||||
const int val = opt->getInt();
|
||||
const bool out_of_range = val > filament_cnt;
|
||||
const bool is_mixed = !allow_mixed && val > 0 && val <= filament_cnt &&
|
||||
wxGetApp().preset_bundle->is_mixed_filament(val - 1);
|
||||
if (!out_of_range && !is_mixed)
|
||||
return;
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
int new_value = 0;
|
||||
if (out_of_range) {
|
||||
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
|
||||
if (conf_temp != nullptr && conf_temp->has(key))
|
||||
new_value = conf_temp->opt_int(key);
|
||||
}
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
|
||||
apply(config, &new_conf);
|
||||
};
|
||||
for (const char* key : physical_only_keys)
|
||||
reset_invalid_filament(key, false);
|
||||
for (const char* key : feature_keys)
|
||||
reset_invalid_filament(key, true);
|
||||
|
||||
// Sub-layer splitting divides each layer by the mix ratio; an adaptive layer profile makes
|
||||
// those sub-layer heights vary per layer, which degrades the blend. Warn once per enable.
|
||||
{
|
||||
static bool s_mixed_sublayer_warned = false;
|
||||
bool sublayer_on = config->opt_bool("enable_mixed_color_sublayer");
|
||||
if (sublayer_on && !s_mixed_sublayer_warned &&
|
||||
wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
|
||||
bool has_variable_layer = false;
|
||||
for (const auto* obj : wxGetApp().model().objects) {
|
||||
if (obj->layer_height_profile.get().size() > 4) {
|
||||
has_variable_layer = true;
|
||||
break;
|
||||
}
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
|
||||
apply(config, &new_conf);
|
||||
}
|
||||
if (has_variable_layer) {
|
||||
MessageDialog dialog(m_msg_dlg_parent,
|
||||
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
|
||||
"", wxICON_WARNING | wxOK);
|
||||
dialog.show_dsa_button();
|
||||
is_msg_dlg_already_exist = true;
|
||||
dialog.ShowModal();
|
||||
is_msg_dlg_already_exist = false;
|
||||
if (dialog.get_checkbox_state())
|
||||
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
|
||||
s_mixed_sublayer_warned = true;
|
||||
}
|
||||
}
|
||||
if (!sublayer_on)
|
||||
s_mixed_sublayer_warned = false;
|
||||
}
|
||||
|
||||
if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None &&
|
||||
|
||||
@@ -385,7 +385,7 @@ public:
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
/// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
|
||||
void propagate_value() ;
|
||||
void propagate_value() override;
|
||||
|
||||
void set_value(const std::string& value, bool change_event = false) {
|
||||
m_disable_change_event = !change_event;
|
||||
@@ -440,7 +440,7 @@ public:
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
|
||||
void propagate_value();
|
||||
void propagate_value() override;
|
||||
|
||||
/* Under OSX: wxBitmapComboBox->GetWindowStyle() returns some weard value,
|
||||
* so let use a flag, which has TRUE value for a control without wxCB_READONLY style
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
@@ -28,6 +31,113 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from,
|
||||
}
|
||||
}
|
||||
|
||||
static std::string to_hex(const wxColour& c)
|
||||
{
|
||||
return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString();
|
||||
}
|
||||
|
||||
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights)
|
||||
{
|
||||
const size_t n = std::min(cols.size(), weights.size());
|
||||
std::vector<std::string> hex_colors;
|
||||
std::vector<int> int_weights;
|
||||
hex_colors.reserve(n);
|
||||
int_weights.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
hex_colors.push_back(to_hex(cols[i]));
|
||||
// Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi;
|
||||
// only relative magnitude matters.
|
||||
int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000.0)));
|
||||
}
|
||||
wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights));
|
||||
return blended.IsOk() ? blended : wxColour(128, 128, 128);
|
||||
}
|
||||
|
||||
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
||||
const wxColour& second,
|
||||
const Slic3r::GradientCurve& curve,
|
||||
int steps)
|
||||
{
|
||||
std::vector<wxColour> ramp;
|
||||
if (steps <= 0 || curve.points.size() < 2) return ramp;
|
||||
|
||||
ramp.reserve(steps);
|
||||
for (int i = 0; i < steps; ++i) {
|
||||
const double t = (steps > 1) ? (i + 0.5) / steps : 0.5;
|
||||
const double r1 = Slic3r::sample_gradient_curve(curve, t);
|
||||
ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1}));
|
||||
}
|
||||
return ramp;
|
||||
}
|
||||
|
||||
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
|
||||
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
|
||||
// endpoints, otherwise the 0.10 -> 0.90 default.
|
||||
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
|
||||
{
|
||||
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
|
||||
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
|
||||
Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]);
|
||||
if (custom.points.size() >= 2) return custom;
|
||||
}
|
||||
|
||||
double start = kGradientMinRatio, end = kGradientMaxRatio;
|
||||
const auto* range_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_range");
|
||||
if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) {
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
float v0 = 0, v1 = 0;
|
||||
if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 &&
|
||||
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
|
||||
start = v0;
|
||||
end = v1;
|
||||
}
|
||||
}
|
||||
|
||||
Slic3r::GradientCurve curve;
|
||||
curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}};
|
||||
return curve;
|
||||
}
|
||||
|
||||
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps)
|
||||
{
|
||||
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
|
||||
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
|
||||
if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {};
|
||||
if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {};
|
||||
if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {};
|
||||
if (slot >= comp_opt->values.size()) return {};
|
||||
|
||||
// Only two-component slots fade; anything else stays on the plain blended swatch.
|
||||
const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]);
|
||||
if (comp_ids.size() != 2) return {};
|
||||
|
||||
auto component_colour = [&](unsigned int id) {
|
||||
wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour();
|
||||
return c.IsOk() ? c : wxColour("#D9D9D9");
|
||||
};
|
||||
|
||||
// Both gradient_range and the curve express the *first* component's ratio over Z, so
|
||||
// the components stay in config order and the curve alone decides which end is which.
|
||||
return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]),
|
||||
mixed_gradient_curve(cfg, slot), steps);
|
||||
}
|
||||
|
||||
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp)
|
||||
{
|
||||
if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return;
|
||||
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
for (int y = 0; y < rect.height; ++y) {
|
||||
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
|
||||
// Mapping over height - 1 puts both ends of the ramp on screen even in a short swatch.
|
||||
const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5;
|
||||
dc.SetBrush(wxBrush(ramp[static_cast<size_t>(t * (ramp.size() - 1) + 0.5)]));
|
||||
dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper struct to hold bitmap and DC
|
||||
struct BitmapDC {
|
||||
wxBitmap bitmap;
|
||||
@@ -47,6 +157,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) {
|
||||
return BitmapDC(size);
|
||||
}
|
||||
|
||||
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size)
|
||||
{
|
||||
if (ramp.empty()) return wxNullBitmap;
|
||||
|
||||
BitmapDC bdc = init_bitmap_dc(size);
|
||||
if (!bdc.dc.IsOk()) return wxNullBitmap;
|
||||
|
||||
fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp);
|
||||
|
||||
bdc.dc.SelectObject(wxNullBitmap);
|
||||
return bdc.bitmap;
|
||||
}
|
||||
|
||||
// Check if a color is transparent (alpha == 0)
|
||||
static bool is_transparent_color(const wxColour& color) {
|
||||
return color.Alpha() == 0;
|
||||
@@ -265,4 +388,65 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
|
||||
}
|
||||
}
|
||||
|
||||
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
|
||||
const Slic3r::DynamicPrintConfig& cfg)
|
||||
{
|
||||
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
const auto* ratio_opt = cfg.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
|
||||
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
|
||||
if (!is_mixed_opt || !comp_opt) return;
|
||||
|
||||
const size_t n = is_mixed_opt->values.size();
|
||||
if (colors.size() < n) colors.resize(n);
|
||||
|
||||
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
|
||||
const auto kFallback = wxColour(128, 128, 128, 255);
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!is_mixed_opt->values[i]) continue;
|
||||
|
||||
if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; }
|
||||
auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]);
|
||||
if (comp_ids.empty()) { colors[i] = kFallback; continue; }
|
||||
|
||||
bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i];
|
||||
std::vector<unsigned int> use_ids = comp_ids;
|
||||
std::vector<int> weights;
|
||||
|
||||
if (is_gradient && comp_ids.size() >= 2) {
|
||||
use_ids = { comp_ids.front(), comp_ids.back() };
|
||||
weights = { 5000, 5000 };
|
||||
} else {
|
||||
auto ratios_d = Slic3r::parse_mixed_ratios(
|
||||
(ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{},
|
||||
comp_ids.size());
|
||||
weights.reserve(comp_ids.size());
|
||||
for (double r : ratios_d)
|
||||
weights.push_back(static_cast<int>(std::lround(r * 10000.0)));
|
||||
}
|
||||
|
||||
std::vector<std::string> hex_colors;
|
||||
hex_colors.reserve(use_ids.size());
|
||||
bool any_invalid = false;
|
||||
for (unsigned int id : use_ids) {
|
||||
if (id == 0 || id > colors.size()) { any_invalid = true; break; }
|
||||
wxColour c = colors[id - 1];
|
||||
if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) {
|
||||
hex_colors.push_back(to_hex(c));
|
||||
} else if (colour_opt && (id - 1) < colour_opt->values.size()) {
|
||||
hex_colors.push_back(colour_opt->values[id - 1]);
|
||||
} else {
|
||||
any_invalid = true; break;
|
||||
}
|
||||
}
|
||||
if (any_invalid) { colors[i] = kFallback; continue; }
|
||||
|
||||
std::string hex = Slic3r::blend_color_multi(hex_colors, weights);
|
||||
wxColour blended(hex);
|
||||
if (!blended.IsOk()) blended = kFallback;
|
||||
colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255);
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <wx/gdicmn.h>
|
||||
#include <vector>
|
||||
|
||||
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
|
||||
// force-included pch (the GUI test suite includes it directly).
|
||||
namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
|
||||
@@ -28,6 +32,37 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors,
|
||||
const wxSize& size,
|
||||
bool force_gradient = false);
|
||||
|
||||
// Blend colours at the given relative weights through blend_color_multi, so a measured
|
||||
// real-world mix is used where one exists instead of a plain channel lerp.
|
||||
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights);
|
||||
|
||||
// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the
|
||||
// model's height, the curve gives the first component's ratio at t, and the two
|
||||
// components are blended at that ratio through blend_n_colors. Entry 0 is the bottom
|
||||
// of the model, the last entry its top.
|
||||
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
||||
const wxColour& second,
|
||||
const Slic3r::GradientCurve& curve,
|
||||
int steps);
|
||||
|
||||
// Same ramp for a project config slot, resolving components, colours and curve (or the
|
||||
// linear gradient_range fallback) from cfg. Returns empty for any slot that is not a
|
||||
// two-component gradient mixed filament. steps is the ramp's resolution; pass the
|
||||
// destination's height in pixels.
|
||||
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
|
||||
|
||||
// Fill rect with a ramp, ramp.front() along the bottom edge.
|
||||
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
|
||||
|
||||
// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp.
|
||||
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size);
|
||||
|
||||
// Recompute blended representative colors for mixed (virtual) filament slots.
|
||||
// Reads mixed-filament config keys from cfg and writes back into colors[i]
|
||||
// for every slot where filament_is_mixed[i] is true.
|
||||
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
|
||||
const Slic3r::DynamicPrintConfig& cfg);
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_
|
||||
@@ -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);
|
||||
@@ -8902,7 +8911,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
|
||||
}
|
||||
else {
|
||||
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice()))
|
||||
// A plate using a mixed filament whose components are broken cannot be sliced,
|
||||
// so surface that on the plate toolbar the same way an unsliceable plate is.
|
||||
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice())
|
||||
|| wxGetApp().plater()->sidebar().has_broken_mixed_filament(plate_list.get_plate(i)))
|
||||
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
|
||||
else {
|
||||
if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f)
|
||||
@@ -9669,6 +9681,13 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
}
|
||||
}
|
||||
}
|
||||
// ORCA: the loop above only labels a slot whose preset was found in the preset collection,
|
||||
// while the render loop below iterates extruder_num. Pad the label arrays so a slot without a
|
||||
// matching preset cannot index past them; a garbage std::string crashes ImGui::CalcTextSize.
|
||||
while (int(filament_text_first_line.size()) < extruder_num) {
|
||||
filament_text_first_line.emplace_back();
|
||||
filament_text_second_line.emplace_back();
|
||||
}
|
||||
|
||||
ImGuiWrapper& imgui = *wxGetApp().imgui();
|
||||
const float canvas_w = float(get_canvas_size().get_width());
|
||||
@@ -9698,6 +9717,10 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
bool disabled = !wxGetApp().plater()->can_fillcolor();
|
||||
ColorRGBA rgba;
|
||||
|
||||
// Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than
|
||||
// the single blended colour in `colors`. Every other slot's ramp is empty.
|
||||
const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
|
||||
|
||||
for (int i = 0; i < extruder_num; i++) {
|
||||
if (i > 0)
|
||||
ImGui::SameLine();
|
||||
@@ -9711,6 +9734,8 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max))
|
||||
wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1));
|
||||
}
|
||||
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty())
|
||||
ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]);
|
||||
if (ImGui::IsItemHovered() && i < 9) {
|
||||
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
|
||||
@@ -9726,7 +9751,13 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
|
||||
const float text_offset_y = 4.0f * em_unit * f_scale;
|
||||
for (int i = 0; i < extruder_num; i++) {
|
||||
decode_color(colors[i], rgba);
|
||||
// A gradient slot's swatch shows its fade instead of the blended colour in `colors`, so the
|
||||
// labels take their contrast from the colour printed at the middle of the fade they sit on.
|
||||
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) {
|
||||
const wxColour& c = gradient_ramps[i][gradient_ramps[i].size() / 2];
|
||||
rgba = ColorRGBA(c.Red(), c.Green(), c.Blue(), c.Alpha());
|
||||
} else
|
||||
decode_color(colors[i], rgba);
|
||||
float gray = 0.299 * rgba.r_uchar() + 0.587 * rgba.g_uchar() + 0.114 * rgba.b_uchar();
|
||||
ImVec4 text_color = gray < 80 ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0, 0, 0, 1.0f);
|
||||
|
||||
@@ -10570,6 +10601,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;
|
||||
@@ -10618,6 +10652,14 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLNozzleFilamentIncompatible, NotificationLevel::WarningNotificationLevel);
|
||||
}
|
||||
}
|
||||
else if (warning == EWarning::SingleExtruderMixedFilament) {
|
||||
// Close by type: check_single_extruder_mixed_filament_risk() clears the shared text
|
||||
// buffer on every call, so a close-by-text would miss once the risk is gone.
|
||||
if (state)
|
||||
notification_manager.push_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel, text);
|
||||
else
|
||||
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel);
|
||||
}
|
||||
else {
|
||||
if (state)
|
||||
notification_manager.push_plater_warning_notification(text);
|
||||
|
||||
@@ -391,6 +391,7 @@ class GLCanvas3D
|
||||
PrimeTowerOutside,
|
||||
NozzleFilamentIncompatible,
|
||||
MixtureFilamentIncompatible,
|
||||
SingleExtruderMixedFilament,
|
||||
FlushingVolumeZero
|
||||
};
|
||||
|
||||
|
||||
@@ -521,10 +521,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
|
||||
/* FT_GCODE */ { L("G-code files"), { ".gcode"sv} },
|
||||
#ifdef __APPLE__
|
||||
/* FT_MODEL */
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
|
||||
#else
|
||||
/* FT_MODEL */
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}},
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}},
|
||||
#endif
|
||||
/* FT_ZIP */ { L("ZIP files"), { ".zip"sv } },
|
||||
/* FT_PROJECT */ { L("Project files"), { ".3mf"sv} },
|
||||
@@ -8905,7 +8905,11 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch
|
||||
if (printer_technology == ptFFF && !edited_printer_preset.config.opt_bool("single_extruder_multi_material")) {
|
||||
auto* nozzle_diameter = edited_printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
if (nozzle_diameter) {
|
||||
preset_bundle->set_num_filaments(nozzle_diameter->values.size());
|
||||
// Mixed-color slots are virtual filaments kept at the tail of the list, so they have no
|
||||
// nozzle of their own. Sizing to the nozzle count alone would silently drop the mixes of
|
||||
// a just-loaded project, and update_extruder_count() would then strip the facets painted
|
||||
// with them.
|
||||
preset_bundle->set_num_filaments(nozzle_diameter->values.size() + preset_bundle->num_mixed_filaments());
|
||||
}
|
||||
}
|
||||
this->plater()->set_printer_technology(printer_technology);
|
||||
|
||||
@@ -1656,16 +1656,16 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
|
||||
{
|
||||
wxMenu *menu = &m_filament_action_menu;
|
||||
|
||||
if (init) {
|
||||
// ORCA rebuild menu everytime instead checking existing of every item then deleting
|
||||
while (menu->GetMenuItemCount() > 0)
|
||||
menu->Destroy(menu->FindItemByPosition(0));
|
||||
|
||||
//if (init) { //
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) {
|
||||
plater()->sidebar().edit_filament(); }, "", nullptr,
|
||||
[]() { return true; }, m_parent);
|
||||
}
|
||||
|
||||
const int item_id = menu->FindItem(_L("Merge with"));
|
||||
if (item_id != wxNOT_FOUND)
|
||||
menu->Destroy(item_id);
|
||||
//}
|
||||
|
||||
wxMenu* sub_menu = new wxMenu();
|
||||
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
|
||||
@@ -1684,11 +1684,15 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
|
||||
append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "",
|
||||
[filaments_cnt]() { return filaments_cnt > 1; }, m_parent);
|
||||
|
||||
// Decompose a target colour into a printable mix of the loaded filaments. Placed before the
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Decompose Color"), "", [](wxCommandEvent&) {
|
||||
plater()->sidebar().decompose_filament_color(kSidebarContextMenuFilamentId); }, "", nullptr,
|
||||
[]() { return plater()->sidebar().combos_filament().size() >= 2; }, m_parent);
|
||||
|
||||
menu->AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete
|
||||
|
||||
// ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS
|
||||
const int delete_id = menu->FindItem(_L("Delete"));
|
||||
if (delete_id != wxNOT_FOUND)
|
||||
menu->Destroy(delete_id);
|
||||
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) {
|
||||
plater()->sidebar().delete_filament(-2); }, "", nullptr,
|
||||
|
||||
@@ -3233,6 +3233,24 @@ void ObjectList::merge(bool to_multipart_object)
|
||||
|
||||
void ObjectList::layers_editing()
|
||||
{
|
||||
// Height ranges give each range its own layer height, varying the mixed sub-layer heights just
|
||||
// like an adaptive profile, so this raises the same warning as variable layer height and shares
|
||||
// its do-not-show-again flag.
|
||||
const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
if (print_config.opt_bool("enable_mixed_color_sublayer")) {
|
||||
if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
|
||||
// Orca: parent to the plater like the sibling site in Plater::priv::on_action_layersediting
|
||||
// (BBS passes nullptr, which MsgDialog remaps to the main frame).
|
||||
MessageDialog dlg(wxGetApp().plater(),
|
||||
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
|
||||
_L("Warning"), wxICON_WARNING | wxOK);
|
||||
dlg.show_dsa_button();
|
||||
dlg.ShowModal();
|
||||
if (dlg.get_checkbox_state())
|
||||
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
|
||||
}
|
||||
}
|
||||
|
||||
const Selection& selection = scene_selection();
|
||||
const int obj_idx = selection.get_object_idx();
|
||||
wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ?
|
||||
|
||||
@@ -85,7 +85,7 @@ public:
|
||||
void update_model_object();
|
||||
//ClippingPlane get_sla_clipping_plane() const;
|
||||
|
||||
bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); }
|
||||
bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); }
|
||||
|
||||
bool wants_enter_leave_snapshots() const override { return true; }
|
||||
std::string get_gizmo_entering_text() const override { return _u8L("Entering Brim Ears"); }
|
||||
|
||||
@@ -75,7 +75,7 @@ protected:
|
||||
virtual void on_render() override;
|
||||
virtual void on_set_state() override;
|
||||
virtual CommonGizmosDataID on_get_requirements() const override;
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit);
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
void on_load(cereal::BinaryInputArchive &ar) override;
|
||||
void on_save(cereal::BinaryOutputArchive &ar) const override;
|
||||
|
||||
@@ -78,6 +78,9 @@ void GLGizmoMmuSegmentation::init_extruders_data()
|
||||
m_extruders_colors = wxGetApp().plater()->get_extruders_colors();
|
||||
m_selected_extruder_idx = 0;
|
||||
|
||||
m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
|
||||
m_gradient_ramps.resize(m_extruders_colors.size());
|
||||
|
||||
// keep remap table consistent with current extruder count
|
||||
m_extruder_remap.resize(m_extruders_colors.size());
|
||||
for (size_t i = 0; i < m_extruder_remap.size(); ++i)
|
||||
@@ -305,15 +308,32 @@ void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y)
|
||||
}
|
||||
|
||||
// ORCA
|
||||
bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
|
||||
bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
|
||||
{
|
||||
// Inset of the frame stroked below, which is what trims the swatch down to its visible shape.
|
||||
const float frame_inset = 1.5f;
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
std::string label_id = std::to_string(idx) + id_str + std::to_string(idx);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 size = ImVec2(27.f * scale, 27.f * scale);
|
||||
ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color);
|
||||
ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1));
|
||||
bool dark_tone = (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
|
||||
// Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade.
|
||||
const std::vector<wxColour>* gradient = gradient_of(idx - 1);
|
||||
// The centered slot number sits at the swatch's mid height, so take its contrast from the colour
|
||||
// printed there rather than from the slot's blended color.
|
||||
bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 :
|
||||
(0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
|
||||
|
||||
// Paint a gradient mixed filament's fade before the button and keep the button transparent, so
|
||||
// the slot number and the frame below stay on top of it. The bands cannot round their corners,
|
||||
// so the fade is inset to the frame, which masks it into the shape a plain color slot gets.
|
||||
if (gradient) {
|
||||
ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale},
|
||||
{pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient);
|
||||
color_vec.w = 0.f; // let the fade show through
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding , 7.f * scale);
|
||||
@@ -329,7 +349,7 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, cons
|
||||
auto drawBorder = [&](float d, float r, float t, ImU32 col) {
|
||||
draw_list->AddRect({pos.x + d * scale, pos.y + d * scale}, {pos.x + size.x - d * scale , pos.y + size.y - d * scale}, col, r * scale, 0, t * scale);
|
||||
};
|
||||
drawBorder(1.5f, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
|
||||
drawBorder(frame_inset, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
|
||||
if(active)
|
||||
drawBorder(.5f, 4.f , 2.f, br_color);
|
||||
else
|
||||
@@ -433,7 +453,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
|
||||
m_selected_extruder_idx = extruder_idx;
|
||||
}
|
||||
|
||||
if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
|
||||
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
|
||||
}
|
||||
// ORCA: Remap filaments section (Border only, Title in border).
|
||||
// Styled as a panel for visual grouping.
|
||||
@@ -731,6 +751,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
|
||||
continue;
|
||||
|
||||
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
|
||||
// A volume may be assigned to a mixed-color slot, whose index can sit past the
|
||||
// physical colour list; fall back to the first colour rather than reading OOB.
|
||||
if (extruder_idx >= (int)m_extruders_colors.size())
|
||||
extruder_idx = 0;
|
||||
std::vector<ColorRGBA> ebt_colors;
|
||||
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]);
|
||||
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
|
||||
@@ -753,6 +777,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors()
|
||||
TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
|
||||
int extruder_idx = m_volumes_extruder_idxs[i];
|
||||
int extruder_color_idx = std::max(0, extruder_idx - 1);
|
||||
// A mixed-color slot can index past the physical colour list; fall back to the first colour.
|
||||
if (extruder_color_idx >= (int)m_extruders_colors.size())
|
||||
extruder_color_idx = 0;
|
||||
std::vector<ColorRGBA> ebt_colors;
|
||||
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
|
||||
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
|
||||
|
||||
@@ -73,11 +73,10 @@ public:
|
||||
|
||||
void data_changed(bool is_serializing) override;
|
||||
|
||||
// TriangleSelector::serialization/deserialization has a limit to store 19 different states.
|
||||
// EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored.
|
||||
// When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization
|
||||
// will be also extended to support additional states, requiring at least one state to remain free out of 19 states.
|
||||
static const constexpr size_t EXTRUDERS_LIMIT = 16;
|
||||
// The paint material limit follows EnforcerBlockerType::ExtruderMax: TriangleSelector
|
||||
// serialization covers the extended (17..32) range through an escape nibble. Mixed-color
|
||||
// filaments occupy ordinary slots, so they draw from the same budget as physical ones.
|
||||
static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
|
||||
|
||||
const float get_cursor_radius_min() const override { return CursorRadiusMin; }
|
||||
|
||||
@@ -116,6 +115,10 @@ protected:
|
||||
|
||||
// Filament remap feature
|
||||
std::vector<size_t> m_extruder_remap; // index → target extruder index
|
||||
// Colours each gradient mixed filament actually prints, bottom of the model first, mirrored
|
||||
// from Plater so the extruder swatches draw the same fade the editor previews. Plain
|
||||
// filament slots keep an empty ramp.
|
||||
std::vector<std::vector<wxColour>> m_gradient_ramps;
|
||||
// ORCA: Cache used filaments to filter UI
|
||||
std::set<size_t> m_used_filaments; // Set of used filament indices (cached)
|
||||
|
||||
@@ -137,7 +140,13 @@ private:
|
||||
void init_model_triangle_selectors();
|
||||
|
||||
// ORCA
|
||||
bool draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
|
||||
bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
|
||||
// Gradient ramp of a filament slot, or nullptr when the slot is a plain single color
|
||||
// filament. A non-null result is never empty.
|
||||
const std::vector<wxColour>* gradient_of(int idx) const
|
||||
{
|
||||
return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr;
|
||||
}
|
||||
|
||||
// BBS
|
||||
void update_triangle_selectors_colors();
|
||||
|
||||
@@ -67,7 +67,7 @@ protected:
|
||||
void on_register_raycasters_for_picking() override;
|
||||
void on_unregister_raycasters_for_picking() override;
|
||||
//BBS: GUI refactor: add object manipulation
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit);
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
private:
|
||||
double calc_projection(const UpdateData& data) const;
|
||||
|
||||
@@ -89,7 +89,7 @@ protected:
|
||||
virtual void on_register_raycasters_for_picking() override;
|
||||
virtual void on_unregister_raycasters_for_picking() override;
|
||||
//BBS: GUI refactor: add object manipulation
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit);
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
private:
|
||||
void render_grabbers_connection(unsigned int id_1, unsigned int id_2, const ColorRGBA& color);
|
||||
|
||||
@@ -998,16 +998,40 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
|
||||
keyCode = keyCode- WXK_NUMPAD0+'0';
|
||||
}
|
||||
if (keyCode >= '0' && keyCode <= '9') {
|
||||
if (keyCode == '1' && !m_timer_set_color.IsRunning()) {
|
||||
// The paint palette reaches EXTRUDERS_LIMIT slots (mixed-color filaments take
|
||||
// ordinary slots too), so any leading digit that can start a valid two-digit
|
||||
// number waits briefly for a second one.
|
||||
const int digit = keyCode - '0';
|
||||
const int shortcut_max = int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT);
|
||||
auto can_start_two_digit = [shortcut_max](int d) { return d > 0 && d * 10 <= shortcut_max; };
|
||||
auto select = [mmu_seg](int number) { return number > 0 && mmu_seg->on_number_key_down(number); };
|
||||
|
||||
if (m_timer_set_color.IsRunning() && m_pending_color_shortcut_tens > 0) {
|
||||
const int two_digit = m_pending_color_shortcut_tens * 10 + digit;
|
||||
const int pending = m_pending_color_shortcut_tens;
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
m_timer_set_color.Stop();
|
||||
if (two_digit <= shortcut_max) {
|
||||
processed = select(two_digit);
|
||||
} else {
|
||||
// Out of range: commit the pending digit, then treat this one as new input.
|
||||
processed = select(pending);
|
||||
if (can_start_two_digit(digit)) {
|
||||
m_pending_color_shortcut_tens = digit;
|
||||
m_timer_set_color.StartOnce(500);
|
||||
processed = true;
|
||||
} else {
|
||||
processed = select(digit) || processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (can_start_two_digit(digit)) {
|
||||
m_pending_color_shortcut_tens = digit;
|
||||
m_timer_set_color.StartOnce(500);
|
||||
processed = true;
|
||||
}
|
||||
else if (keyCode < '7' && m_timer_set_color.IsRunning()) {
|
||||
processed = mmu_seg->on_number_key_down(keyCode - '0'+10);
|
||||
m_timer_set_color.Stop();
|
||||
}
|
||||
else {
|
||||
processed = mmu_seg->on_number_key_down(keyCode - '0');
|
||||
processed = select(digit);
|
||||
}
|
||||
}
|
||||
else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') {
|
||||
@@ -1054,11 +1078,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
|
||||
|
||||
void GLGizmosManager::on_set_color_timer(wxTimerEvent& evt)
|
||||
{
|
||||
if (m_current == MmSegmentation) {
|
||||
// No second digit arrived in time: commit the pending leading digit on its own.
|
||||
if (m_current == MmSegmentation && m_pending_color_shortcut_tens > 0) {
|
||||
GLGizmoMmuSegmentation* mmu_seg = dynamic_cast<GLGizmoMmuSegmentation*>(get_current());
|
||||
mmu_seg->on_number_key_down(1);
|
||||
m_parent.set_as_dirty();
|
||||
if (mmu_seg != nullptr) {
|
||||
mmu_seg->on_number_key_down(m_pending_color_shortcut_tens);
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
}
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
}
|
||||
|
||||
void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot)
|
||||
|
||||
@@ -144,6 +144,8 @@ private:
|
||||
|
||||
//When there are more than 9 colors, shortcut key coloring
|
||||
wxTimer m_timer_set_color;
|
||||
// Leading digit of a two-digit color shortcut still waiting for its second digit.
|
||||
int m_pending_color_shortcut_tens = 0;
|
||||
void on_set_color_timer(wxTimerEvent& evt);
|
||||
|
||||
// key MENU_ICON_NAME, value = ImtextureID
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
#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 ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing.
|
||||
// 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. Resolved through StateColor::darkModeColorFor()
|
||||
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
|
||||
// #818183, #262E30 -> #EFEFF0, #ACACAC -> #65656A, *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
|
||||
const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements
|
||||
|
||||
// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve
|
||||
// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than
|
||||
// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets 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.
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
GradientCurveEditor::~GradientCurveEditor()
|
||||
{
|
||||
// See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it
|
||||
// still holds the capture wedges mouse input for the whole application.
|
||||
if (HasCapture())
|
||||
ReleaseMouse();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const
|
||||
{
|
||||
const wxRect r = plot_rect();
|
||||
// y axis is inverted: y=1 should sit at the top.
|
||||
return wxPoint2DDouble(r.x + x * r.width, r.y + (1.0 - y) * r.height);
|
||||
}
|
||||
|
||||
wxPoint GradientCurveEditor::data_to_px(double x, double y) const
|
||||
{
|
||||
const wxPoint2DDouble p = data_to_px_f(x, y);
|
||||
return wxPoint(static_cast<int>(std::lround(p.m_x)), static_cast<int>(std::lround(p.m_y)));
|
||||
}
|
||||
|
||||
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);
|
||||
// Softer than axis_color: the curve outline only has to lift the curve off the
|
||||
// background, it must not compete with the structural axis / grid.
|
||||
const wxColour outline_color = StateColor::darkModeColorFor(kOutlineColor);
|
||||
|
||||
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);
|
||||
// The curve and its anchors are drawn straight on the graphics context so their
|
||||
// coordinates stay sub-pixel accurate (see data_to_px_f).
|
||||
wxGraphicsContext* gc = dc.GetGraphicsContext();
|
||||
|
||||
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 || !gc)
|
||||
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<wxPoint2DDouble> {
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
std::vector<wxPoint2DDouble> 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_f(x, vy));
|
||||
}
|
||||
return poly;
|
||||
};
|
||||
|
||||
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
|
||||
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
|
||||
// forwards it here while keeping its own cached state in sync for later dc drawing.
|
||||
auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) {
|
||||
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
|
||||
gc->StrokeLines(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.
|
||||
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.
|
||||
// Drawn on the graphics context with a sub-pixel center so the ring stays centered on the
|
||||
// curve instead of drifting up to half a pixel off it; pen and brush go through the dc for
|
||||
// the same reason as in draw_polyline above.
|
||||
const double 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 wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy);
|
||||
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
|
||||
}
|
||||
}
|
||||
|
||||
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. Bending the segment without
|
||||
// inserting an anchor is not an option: a single cubic between two existing
|
||||
// anchors 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
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef slic3r_GradientCurveEditor_hpp_
|
||||
#define slic3r_GradientCurveEditor_hpp_
|
||||
|
||||
#include <vector>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/geometry.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));
|
||||
|
||||
~GradientCurveEditor() override;
|
||||
|
||||
// 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;
|
||||
// Sub-pixel accurate mapping, used for drawing: rounding the curve vertices to whole
|
||||
// pixels leaves a staircase that anti-aliasing cannot smooth out, and the step is
|
||||
// twice as coarse on 2x (Retina) displays.
|
||||
wxPoint2DDouble data_to_px_f(double x, double y) 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_
|
||||
@@ -2404,6 +2404,25 @@ void ImGuiWrapper::draw(
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiWrapper::draw_gradient_ramp(ImDrawList *draw_list, const ImVec2 &top_left, const ImVec2 &bottom_right, const std::vector<wxColour> &ramp)
|
||||
{
|
||||
if (draw_list == nullptr || ramp.empty() || bottom_right.x <= top_left.x || bottom_right.y <= top_left.y)
|
||||
return;
|
||||
|
||||
const int rows = std::max(1, (int) std::lround(bottom_right.y - top_left.y));
|
||||
const float row_h = (bottom_right.y - top_left.y) / rows;
|
||||
const size_t last = ramp.size() - 1;
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
|
||||
const double t = (rows > 1) ? (double) (rows - 1 - r) / (rows - 1) : 0.5;
|
||||
const wxColour &c = ramp[(size_t) (t * last + 0.5)];
|
||||
// The bottom row snaps to the rect's edge so rounding never leaves a sliver uncovered.
|
||||
const float y0 = top_left.y + r * row_h;
|
||||
const float y1 = (r + 1 == rows) ? bottom_right.y : top_left.y + (r + 1) * row_h;
|
||||
draw_list->AddRectFilled({top_left.x, y0}, {bottom_right.x, y1}, IM_COL32(c.Red(), c.Green(), c.Blue(), c.Alpha()));
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) {
|
||||
auto draw_list = ImGui::GetOverlayDrawList();
|
||||
draw_list->AddCircle(position, radius, color, num_segments, thickness);
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
|
||||
#include <wx/colour.h>
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
@@ -299,6 +301,20 @@ public:
|
||||
int num_segments = 0,
|
||||
float thickness = 4.f);
|
||||
|
||||
/// <summary>
|
||||
/// Fill a rect with a filament gradient ramp, one band per pixel row, ramp.front() along
|
||||
/// the bottom edge. Bands rather than one interpolated rect, because the ramp follows the
|
||||
/// slot's gradient curve and ImGui's corner interpolation could only draw a straight fade.
|
||||
/// </summary>
|
||||
/// <param name="draw_list">Define where to draw it</param>
|
||||
/// <param name="top_left">Upper left corner of the rect</param>
|
||||
/// <param name="bottom_right">Lower right corner of the rect</param>
|
||||
/// <param name="ramp">Colours printed, bottom of the model first</param>
|
||||
static void draw_gradient_ramp(ImDrawList * draw_list,
|
||||
const ImVec2 & top_left,
|
||||
const ImVec2 & bottom_right,
|
||||
const std::vector<wxColour> &ramp);
|
||||
|
||||
/// <summary>
|
||||
/// Check that font ranges contain all chars in string
|
||||
/// (rendered Unicodes are stored in GlyphRanges)
|
||||
|
||||
@@ -2341,6 +2341,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
@@ -0,0 +1,183 @@
|
||||
#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 = {});
|
||||
|
||||
~MixedFilamentDialog();
|
||||
|
||||
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);
|
||||
|
||||
// Reserves the same width on every material row label so the combo boxes line up.
|
||||
static void apply_uniform_label_width(wxStaticText* lbl);
|
||||
// Appends one "Filament N" label + combo row to m_material_rows_sizer. N follows the
|
||||
// number of rows already there, so callers must not renumber anything themselves.
|
||||
void append_material_row();
|
||||
|
||||
// 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};
|
||||
|
||||
// Drag state. The ratio bar and the triangle picker capture the mouse
|
||||
// independently, so they must not share a flag: a mouse-up on one would
|
||||
// otherwise clear the other's flag and skip its ReleaseMouse().
|
||||
bool m_ratio_dragging{false};
|
||||
bool m_tri_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_
|
||||
@@ -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,
|
||||
@@ -172,6 +174,8 @@ enum class NotificationType
|
||||
BBLBedFilamentIncompatible,
|
||||
BBLMixUsePLAAndPETG,
|
||||
BBLNozzleFilamentIncompatible,
|
||||
// A mixed-color filament is printed on a single-nozzle printer (frequent changes and purging).
|
||||
BBLSingleExtruderMixedFilamentRisk,
|
||||
OrcaSharedProfilesAvailable,
|
||||
OrcaCloudAPIError,
|
||||
OrcaSyncConflict,
|
||||
|
||||
@@ -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,50 @@ 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 before
|
||||
// slicing. Multi-nozzle printers keep the components loaded at once and are not affected.
|
||||
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;
|
||||
@@ -6384,6 +6485,31 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
}
|
||||
//parse filament info
|
||||
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
|
||||
|
||||
// Record mixed (virtual) filaments actually used on this plate.
|
||||
// Source is ToolOrdering::used_mixed_filaments (slots that appeared in
|
||||
// layer tools before resolve), persisted on GCodeProcessorResult / Print —
|
||||
// not print->extruders() which only reflects assignment.
|
||||
{
|
||||
std::vector<unsigned int> used_mixed;
|
||||
if (auto *slice_result = m_plate_list[i]->get_slice_result())
|
||||
used_mixed = slice_result->used_mixed_filaments;
|
||||
if (used_mixed.empty() && print)
|
||||
used_mixed = print->get_slice_used_mixed_filaments();
|
||||
if (!used_mixed.empty() && print) {
|
||||
const auto &fila_types = print->config().filament_type.values;
|
||||
const auto &fila_colors = print->config().filament_colour.values;
|
||||
const auto &fila_comps = print->config().filament_mixed_components.values;
|
||||
for (unsigned int fid : used_mixed) {
|
||||
PlateMixedFilamentInfo mixed_info;
|
||||
mixed_info.id = (int) fid + 1;
|
||||
if (fid < fila_types.size()) mixed_info.type = fila_types[fid];
|
||||
if (fid < fila_colors.size()) mixed_info.color = fila_colors[fid];
|
||||
if (fid < fila_comps.size()) mixed_info.components = fila_comps[fid];
|
||||
plate_data_item->mixed_filaments_info.push_back(mixed_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result()
|
||||
<< ", result valid = " << m_plate_list[i]->is_slice_result_valid();
|
||||
@@ -6450,6 +6576,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
|
||||
m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info;
|
||||
gcode_result->warnings = plate_data_list[i]->warnings;
|
||||
gcode_result->filament_maps = plate_data_list[i]->filament_maps;
|
||||
gcode_result->used_mixed_filaments.clear();
|
||||
for (const auto &mixed_info : plate_data_list[i]->mixed_filaments_info) {
|
||||
if (mixed_info.id > 0)
|
||||
gcode_result->used_mixed_filaments.push_back(static_cast<unsigned int>(mixed_info.id - 1));
|
||||
}
|
||||
if (Print *print = dynamic_cast<Print*>(fff_print))
|
||||
print->set_slice_used_mixed_filaments(gcode_result->used_mixed_filaments);
|
||||
|
||||
// Reconstruct the device-side nozzle grouping from the loaded 3mf so
|
||||
// the monitor/preview can map filaments to physical nozzles.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -472,6 +472,31 @@ PlateSettingsDialog::PlateSettingsDialog(wxWindow* parent, const wxString& title
|
||||
m_sizer_main->AddSpacer(FromDIP(5));
|
||||
m_sizer_main->Add(m_other_layers_seq_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30));
|
||||
|
||||
// A mixed-color slot resolves to a different physical filament per layer, so a user-defined
|
||||
// filament order cannot be honoured; grey out the choice and explain that in the dialog.
|
||||
{
|
||||
auto &proj_cfg = wxGetApp().preset_bundle->project_config;
|
||||
auto *is_mixed_opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
if (is_mixed_opt && Slic3r::has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
m_first_layer_print_seq_choice->Enable(false);
|
||||
m_other_layers_seq_panel->enable_seq_choice(false);
|
||||
|
||||
auto *warn_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto *warn_icon = new wxStaticBitmap(this, wxID_ANY, create_scaled_bitmap("warning", this, 16),
|
||||
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
|
||||
auto *warn_text = new wxStaticText(this, wxID_ANY,
|
||||
_L("The filament list contains mixed filaments. Custom filament sequence will not take effect."));
|
||||
warn_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00")));
|
||||
warn_text->SetFont(Label::Body_12);
|
||||
warn_text->Wrap(FromDIP(300));
|
||||
|
||||
warn_sizer->Add(warn_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
warn_sizer->Add(warn_text, 1, wxALIGN_CENTER_VERTICAL, 0);
|
||||
m_sizer_main->AddSpacer(FromDIP(5));
|
||||
m_sizer_main->Add(warn_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30));
|
||||
}
|
||||
}
|
||||
|
||||
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
|
||||
|
||||
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](auto& e) {
|
||||
|
||||
@@ -62,6 +62,9 @@ public:
|
||||
int get_layers_print_seq_choice() { return m_other_layer_print_seq_choice->GetSelection(); };
|
||||
|
||||
std::vector<LayerSeqInfo> get_layers_print_seq_infos() { return m_layer_seq_infos; }
|
||||
// Lets callers grey out the sequence choice (e.g. when a mixed filament makes a
|
||||
// user-defined filament order impossible).
|
||||
void enable_seq_choice(bool enable) { m_other_layer_print_seq_choice->Enable(enable); }
|
||||
|
||||
protected:
|
||||
void append_layer(const LayerSeqInfo* layer_info = nullptr);
|
||||
|
||||
+6996
-5077
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
#include <vector>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <wx/colour.h>
|
||||
#include <wx/panel.h>
|
||||
// BBS
|
||||
#include <wx/notebook.h>
|
||||
@@ -86,6 +87,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 +193,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 +267,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 +332,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 = "-");
|
||||
@@ -384,8 +408,6 @@ public:
|
||||
|
||||
// BBS: restore
|
||||
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false, bool* published_out = nullptr);
|
||||
// To be called when providing a list of files to the GUI slic3r on command line.
|
||||
std::vector<size_t> load_files(const std::vector<std::string>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
|
||||
// to be called on drag and drop
|
||||
bool load_files(const wxArrayString& filenames);
|
||||
|
||||
@@ -571,7 +593,7 @@ public:
|
||||
|
||||
void on_filament_change(size_t filament_idx);
|
||||
void on_filament_count_change(size_t extruders_count);
|
||||
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1);
|
||||
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1, const std::vector<unsigned char>& is_mixed_before_delete = {});
|
||||
std::vector<Slic3r::ColorRGBA> get_extruders_colors();
|
||||
// BBS
|
||||
void on_bed_type_change(BedType bed_type);
|
||||
@@ -586,6 +608,12 @@ public:
|
||||
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const;
|
||||
std::vector<std::string> get_filament_colors_render_info() const;
|
||||
std::vector<std::string> get_filament_color_render_type() const;
|
||||
|
||||
// Per slot, the colours a gradient mixed filament actually prints, sampled bottom (index 0)
|
||||
// to top, so the sidebar, the paint gizmo and the extruder icons draw the same fade the
|
||||
// editor previews rather than a straight blend of two endpoints. A slot that is not a
|
||||
// gradient mixed filament gets an empty ramp. Cached; recomputed when the config changes.
|
||||
const std::vector<std::vector<wxColour>>& get_filament_gradient_ramps() const;
|
||||
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
|
||||
|
||||
void set_global_filament_map_mode(FilamentMapMode mode);
|
||||
|
||||
@@ -79,7 +79,7 @@ public:
|
||||
Bind(wxEVT_LEFT_DOWN, &WikiLabel::OnLeftDown, this);
|
||||
}
|
||||
|
||||
void SetLabel(const wxString& label)
|
||||
void SetLabel(const wxString& label) override
|
||||
{
|
||||
m_label = label;
|
||||
m_last_wrap_width = -1; // force re-wrap
|
||||
|
||||
@@ -163,7 +163,7 @@ public:
|
||||
BedType bedType() const { return m_BedType; }
|
||||
|
||||
virtual void init() override;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const
|
||||
virtual std::map<std::string, std::string> extendedInfo() const override
|
||||
{
|
||||
return {{"bedType", std::to_string(static_cast<int>(m_BedType))},
|
||||
{"timeLapse", std::to_string(m_timeLapse)},
|
||||
@@ -200,7 +200,7 @@ public:
|
||||
PrintHost* printhost);
|
||||
|
||||
virtual void init() override;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const override;
|
||||
|
||||
private:
|
||||
static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_Preview.hpp"
|
||||
@@ -5693,10 +5694,15 @@ void SelectMachineDialog::clone_thumbnail_data() {
|
||||
m_preview_colors_in_thumbnail.resize(m_materialList.size());
|
||||
}
|
||||
while (iter != m_materialList.end()) {
|
||||
int id = iter->first;
|
||||
Material * item = iter->second;
|
||||
MaterialItem *m = item->item;
|
||||
m_preview_colors_in_thumbnail[id] = m->m_material_coloul;
|
||||
// Orca: key the preview colours by filament slot, as m_cur_colors_in_thumbnail and
|
||||
// SyncAmsInfoDialog already do, so recompute_mixed_slot_colors() below can look a mixed
|
||||
// slot's component colours up by id (BBS keys this array by list position).
|
||||
if (item->id >= m_preview_colors_in_thumbnail.size()) {
|
||||
m_preview_colors_in_thumbnail.resize(item->id + 1);
|
||||
}
|
||||
m_preview_colors_in_thumbnail[item->id] = m->m_material_coloul;
|
||||
if (item->id < m_cur_colors_in_thumbnail.size()) {
|
||||
m_cur_colors_in_thumbnail[item->id] = m->m_ams_coloul;
|
||||
}
|
||||
@@ -5706,6 +5712,20 @@ void SelectMachineDialog::clone_thumbnail_data() {
|
||||
}
|
||||
iter++;
|
||||
}
|
||||
|
||||
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
size_t total = 0;
|
||||
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
|
||||
total = opt->values.size();
|
||||
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
|
||||
if (m_cur_colors_in_thumbnail.size() < target)
|
||||
m_cur_colors_in_thumbnail.resize(target);
|
||||
if (m_preview_colors_in_thumbnail.size() < target)
|
||||
m_preview_colors_in_thumbnail.resize(target);
|
||||
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
//copy data
|
||||
auto &data = m_cur_input_thumbnail_data;
|
||||
m_preview_thumbnail_data.reset();
|
||||
@@ -5880,6 +5900,10 @@ void SelectMachineDialog::change_default_normal(int old_filament_id, wxColour te
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Recompute mixed slot colors after physical slot color change
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
ThumbnailData& data = m_cur_input_thumbnail_data;
|
||||
ThumbnailData& no_light_data = m_cur_no_light_thumbnail_data;
|
||||
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {
|
||||
|
||||
@@ -522,7 +522,7 @@ public:
|
||||
bool is_timeout();
|
||||
int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path);
|
||||
void set_print_type(PrintFromType type) {m_print_type = type;};
|
||||
bool Show(bool show);
|
||||
bool Show(bool show) override;
|
||||
void show_init();
|
||||
bool do_ams_mapping(MachineObject *obj_,bool use_ams);
|
||||
bool get_ams_mapping_result(std::string& mapping_array_str, std::string& mapping_array_str2, std::string& ams_mapping_info) const;
|
||||
|
||||
@@ -180,7 +180,7 @@ public:
|
||||
SendToPrinterDialog(Plater *plater = nullptr);
|
||||
~SendToPrinterDialog();
|
||||
|
||||
bool Show(bool show);
|
||||
bool Show(bool show) override;
|
||||
bool is_timeout();
|
||||
void on_rename_click(wxCommandEvent& event);
|
||||
void on_rename_enter();
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "DeviceCore/DevManager.h"
|
||||
#include "DeviceCore/DevMapping.h"
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
@@ -2575,6 +2576,10 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list()
|
||||
m_materialList.clear();
|
||||
m_filaments.clear();
|
||||
|
||||
// Mixed-color slots are virtual: they never occupy a tray, so they must not appear as
|
||||
// AMS sync targets.
|
||||
auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
|
||||
bool use_double_extruder = get_is_double_extruder();
|
||||
if (use_double_extruder) {
|
||||
const auto &project_config = preset_bundle->project_config;
|
||||
@@ -2592,6 +2597,8 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list()
|
||||
auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]);
|
||||
if (extruder >= materials.size() || extruder < 0 || extruder >= display_materials.size())
|
||||
continue;
|
||||
if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder])
|
||||
continue;
|
||||
|
||||
if (contronal_index % SYNC_FLEX_GRID_COL == 0) {
|
||||
wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
@@ -2793,6 +2800,10 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list()
|
||||
m_fix_materialList.clear();
|
||||
m_fix_filaments.clear();
|
||||
|
||||
// Mixed-color slots are virtual: they never occupy a tray, so they must not appear as
|
||||
// AMS sync targets.
|
||||
auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
|
||||
bool use_double_extruder = get_is_double_extruder();
|
||||
if (use_double_extruder) {
|
||||
const auto &project_config = preset_bundle->project_config;
|
||||
@@ -2810,6 +2821,8 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list()
|
||||
auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]);
|
||||
if (extruder >= extruders.size() || extruder < 0 || extruder >= m_ams_combo_info.ams_filament_colors.size())
|
||||
continue;
|
||||
if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder])
|
||||
continue;
|
||||
|
||||
if (contronal_index % SYNC_FLEX_GRID_COL == 0) {
|
||||
wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
@@ -2931,6 +2944,20 @@ void SyncAmsInfoDialog::clone_thumbnail_data()
|
||||
iter++;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
size_t total = 0;
|
||||
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
|
||||
total = opt->values.size();
|
||||
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
|
||||
if (m_cur_colors_in_thumbnail.size() < target)
|
||||
m_cur_colors_in_thumbnail.resize(target);
|
||||
if (m_preview_colors_in_thumbnail.size() < target)
|
||||
m_preview_colors_in_thumbnail.resize(target);
|
||||
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
// copy data
|
||||
auto &data = m_cur_input_thumbnail_data;
|
||||
m_preview_thumbnail_data.reset();
|
||||
@@ -3119,6 +3146,10 @@ void SyncAmsInfoDialog::change_default_normal(int old_filament_id, wxColour temp
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Recompute mixed slot colors after physical slot color change
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
ThumbnailData &data = m_cur_input_thumbnail_data;
|
||||
ThumbnailData &no_light_data = m_cur_no_light_thumbnail_data;
|
||||
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {
|
||||
|
||||
@@ -371,7 +371,7 @@ public:
|
||||
};
|
||||
FinishSyncAmsDialog(InputInfo &input_info);
|
||||
~FinishSyncAmsDialog() override;
|
||||
void deal_ok();
|
||||
void deal_ok() override;
|
||||
void update_info(InputInfo& info);
|
||||
bool Layout() override;
|
||||
|
||||
|
||||
+22
-2
@@ -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"
|
||||
@@ -2181,8 +2182,11 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
new_colors.push_back(new_color);
|
||||
}
|
||||
wxGetApp().preset_bundle->set_num_filaments(num_extruder, new_colors);
|
||||
wxGetApp().plater()->on_filament_count_change(num_extruder);
|
||||
// Mixed-color slots are virtual filaments at the tail of the list with no nozzle of their
|
||||
// own, so they are carried on top of the new extruder count instead of being truncated.
|
||||
const size_t total_filaments = num_extruder + wxGetApp().preset_bundle->num_mixed_filaments();
|
||||
wxGetApp().preset_bundle->set_num_filaments(total_filaments, new_colors);
|
||||
wxGetApp().plater()->on_filament_count_change(total_filaments);
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->update();
|
||||
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
|
||||
}
|
||||
@@ -2629,6 +2633,7 @@ void TabPrint::build()
|
||||
auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height");
|
||||
optgroup->append_single_option_line("layer_height","quality_settings_layer_height");
|
||||
optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height");
|
||||
optgroup->append_single_option_line("enable_mixed_color_sublayer");
|
||||
|
||||
optgroup = page->new_optgroup(L("Line width"), L"param_line_width");
|
||||
optgroup->append_single_option_line("line_width","quality_settings_line_width");
|
||||
@@ -3497,6 +3502,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)
|
||||
|
||||
@@ -514,13 +514,13 @@ public:
|
||||
bool has_key(std::string const &key);
|
||||
|
||||
protected:
|
||||
virtual void activate_selected_page(std::function<void()> throw_if_canceled);
|
||||
virtual void activate_selected_page(std::function<void()> throw_if_canceled) override;
|
||||
|
||||
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
|
||||
|
||||
virtual void notify_changed(ObjectBase * object) = 0;
|
||||
|
||||
virtual void reload_config();
|
||||
virtual void reload_config() override;
|
||||
|
||||
virtual void update_custom_dirty(std::vector<std::string> &dirty_options, std::vector<std::string> &nonsys_options) override;
|
||||
|
||||
@@ -544,6 +544,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;
|
||||
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
|
||||
void SetBitmap(ScalableBitmap &bitmap);
|
||||
|
||||
bool Enable(bool enable = true);
|
||||
bool Enable(bool enable = true) override;
|
||||
|
||||
void Rescale();
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemovePage(size_t n)
|
||||
bool RemovePage(size_t n) override
|
||||
{
|
||||
if (!wxBookCtrlBase::RemovePage(n))
|
||||
return false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user