mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement
This commit is contained in:
+15
-1
@@ -75,7 +75,7 @@ if (SLIC3R_GUI)
|
||||
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX expat)
|
||||
list(APPEND wxWidgets_LIBRARIES ${EXPAT_LIBRARIES})
|
||||
endif ()
|
||||
|
||||
|
||||
# This is an issue in the new wxWidgets cmake build, doesn't deal with librt
|
||||
find_library(LIBRT rt)
|
||||
if(LIBRT)
|
||||
@@ -294,6 +294,16 @@ if (WIN32)
|
||||
endif()
|
||||
|
||||
else ()
|
||||
if (NOT APPLE)
|
||||
set(output_sos_Release "")
|
||||
set(output_sos_Debug "")
|
||||
add_custom_target(OrcaSlicerSosCopy ALL DEPENDS OrcaSlicer)
|
||||
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
orcaslicer_copy_sos(OrcaSlicerSosCopy "Debug" "d" output_sos_Debug)
|
||||
else()
|
||||
orcaslicer_copy_sos(OrcaSlicerSosCopy "Release" "" output_sos_Release)
|
||||
endif()
|
||||
endif()
|
||||
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
|
||||
# On OSX, the name of the binary matches the name of the Application.
|
||||
add_custom_command(TARGET OrcaSlicer POST_BUILD
|
||||
@@ -372,5 +382,9 @@ if (WIN32)
|
||||
install(FILES ${output_dlls_${build_type}} DESTINATION ".")
|
||||
install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python")
|
||||
else ()
|
||||
if (APPLE)
|
||||
else()
|
||||
install(FILES ${output_sos_${build_type}} DESTINATION "${CMAKE_INSTALL_PREFIX}")
|
||||
endif()
|
||||
install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
endif ()
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#define _WIN32_WINNT 0x0502
|
||||
// The standard Windows includes.
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <wchar.h>
|
||||
#include <commctrl.h>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#define _WIN32_WINNT 0x0502
|
||||
// The standard Windows includes.
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <wchar.h>
|
||||
|
||||
@@ -20,6 +20,16 @@ if (SLIC3R_ENC_CHECK)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (ORCA_TOOLS)
|
||||
set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
|
||||
|
||||
# generate_system_cache: pre-generates per-vendor <vendor>.opc files under resources/profiles for CI bundling.
|
||||
add_executable(generate_system_cache generate_system_cache.cpp)
|
||||
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
|
||||
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
|
||||
|
||||
endif()
|
||||
|
||||
# Function that adds source file encoding check to a target
|
||||
# using the above encoding-check binary
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <iostream>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
namespace po = boost::program_options;
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
po::options_description desc("OrcaSlicer System Cache Generator\nUsage");
|
||||
// clang-format off
|
||||
desc.add_options()
|
||||
("help,h", "Show help")
|
||||
#ifdef __APPLE__
|
||||
("path,p", po::value<std::string>()->default_value("../../../../../../../resources/profiles"), "Path to profiles directory")
|
||||
#else
|
||||
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
|
||||
#endif
|
||||
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
|
||||
// clang-format on
|
||||
|
||||
po::variables_map vm;
|
||||
try {
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
if (vm.count("help")) { std::cout << desc << "\n"; return 0; }
|
||||
po::notify(vm);
|
||||
} catch (const po::error& e) {
|
||||
std::cerr << "Error: " << e.what() << "\n" << desc << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string profiles_path = vm["path"].as<std::string>();
|
||||
const int log_level = vm["log_level"].as<int>();
|
||||
|
||||
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
|
||||
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
set_logging_level(log_level);
|
||||
set_data_dir(profiles_path);
|
||||
set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string());
|
||||
|
||||
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
|
||||
if (!fs::exists(user_dir))
|
||||
fs::create_directories(user_dir);
|
||||
|
||||
AppConfig app_config;
|
||||
app_config.set("preset_folder", "default");
|
||||
|
||||
auto preset_bundle = std::make_unique<PresetBundle>();
|
||||
preset_bundle->set_is_validation_mode(true);
|
||||
preset_bundle->set_default_suppressed(true);
|
||||
preset_bundle->set_generate_vendor_caches(true);
|
||||
|
||||
std::cout << "Loading system presets from: " << profiles_path << "\n";
|
||||
|
||||
try {
|
||||
// In validation mode data_dir() is the profiles directory set above, so the
|
||||
// loader writes each <vendor>.opc next to its <vendor>.json as it parses it.
|
||||
preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << "Failed to load presets: " << ex.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t cache_count = 0;
|
||||
for (auto& entry : fs::directory_iterator(profiles_path))
|
||||
if (boost::iends_with(entry.path().string(), ".opc"))
|
||||
++ cache_count;
|
||||
if (cache_count == 0) {
|
||||
std::cerr << "No vendor cache files were generated under " << profiles_path << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Generated " << cache_count << " vendor cache file(s) under " << profiles_path << "\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -83,6 +83,10 @@ copy_shared_object_to_dir() {
|
||||
src_real="$(readlink -f "$src")"
|
||||
dst_name="$(basename "$src_real")"
|
||||
mkdir -p "$dst_dir"
|
||||
if [ "$src_real" = "$dst_dir/$dst_name" ]; then
|
||||
# Already bundled; the dependency resolved from the bundle directory.
|
||||
return 0
|
||||
fi
|
||||
cp -fL "$src_real" "$dst_dir/$dst_name"
|
||||
|
||||
if [ -L "$src" ]; then
|
||||
@@ -96,12 +100,23 @@ copy_shared_object_to_dir() {
|
||||
}
|
||||
|
||||
bundle_dependency_closure() {
|
||||
local dst_dir="$1"
|
||||
local dst_dir
|
||||
dst_dir="$(cd -- "$1" && pwd)"
|
||||
shift
|
||||
|
||||
local -a queue=("$@")
|
||||
local target dep dep_real copied_path
|
||||
local target dep dep_real dep_key copied_path
|
||||
declare -A seen=()
|
||||
# Dependencies are resolved with ldd, which only searches the default
|
||||
# loader path. Deps-built shared libraries (e.g. the FFmpeg stack) are not
|
||||
# installed there and carry no RUNPATH of their own, so once copied into
|
||||
# the bundle ldd can no longer resolve one sibling from another
|
||||
# (libavcodec -> libavutil) and reports it as missing. Extend the loader
|
||||
# path with the bundle directory plus the source directories of files
|
||||
# already bundled, so every library that was resolved once keeps resolving
|
||||
# for its own dependencies. The audit script does the same
|
||||
# (scripts/check_appimage_libs.sh).
|
||||
local -a search_dirs=("$dst_dir")
|
||||
|
||||
while [ ${#queue[@]} -gt 0 ]; do
|
||||
target="${queue[0]}"
|
||||
@@ -122,17 +137,24 @@ bundle_dependency_closure() {
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -n "${seen[$dep_real]}" ]; then
|
||||
# Key dedup on the bundled file rather than the source path: once
|
||||
# ldd resolves a library from the bundle directory (via the
|
||||
# LD_LIBRARY_PATH above) its path is a dst_dir path, which differs
|
||||
# from the source path the first resolution returned. Keying on
|
||||
# the source path would re-copy the file onto itself.
|
||||
dep_key="$dst_dir/$(basename "$dep_real")"
|
||||
if [ -n "${seen[$dep_key]}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
seen[$dep_real]=1
|
||||
seen[$dep_key]=1
|
||||
copy_shared_object_to_dir "$dep" "$dst_dir"
|
||||
search_dirs+=("$(dirname "$dep_real")")
|
||||
copied_path="$dst_dir/$(basename "$dep_real")"
|
||||
if [ -e "$copied_path" ]; then
|
||||
queue+=("$copied_path")
|
||||
fi
|
||||
done < <(appimage_list_direct_dependencies "$target")
|
||||
done < <(LD_LIBRARY_PATH="$(IFS=:; printf '%s' "${search_dirs[*]}")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" appimage_list_direct_dependencies "$target")
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ public:
|
||||
m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {}
|
||||
size_t idx() const { return m_idx; }
|
||||
const BoundingBox& bbox() const { return m_bbox; }
|
||||
Point centroid() const { return (m_bbox.min() + m_bbox.max() / 2); }
|
||||
Point centroid() const { return (m_bbox.min() + m_bbox.max()) / 2; }
|
||||
private:
|
||||
size_t m_idx;
|
||||
BoundingBox m_bbox;
|
||||
|
||||
@@ -856,6 +856,10 @@ std::string AppConfig::load()
|
||||
local_machine.dev_ip = p["dev_ip"].get<std::string>();
|
||||
if (p.contains("printer_type"))
|
||||
local_machine.printer_type = p["printer_type"].get<std::string>();
|
||||
if (p.contains("printer_agent_id"))
|
||||
local_machine.printer_agent_id = p["printer_agent_id"].get<std::string>();
|
||||
if (p.contains("access_code"))
|
||||
local_machine.access_code = p["access_code"].get<std::string>();
|
||||
m_local_machines[local_machine.dev_id] = local_machine;
|
||||
}
|
||||
} else {
|
||||
@@ -1068,6 +1072,8 @@ void AppConfig::save()
|
||||
m_json["dev_name"] = local_machine.second.dev_name;
|
||||
m_json["dev_ip"] = local_machine.second.dev_ip;
|
||||
m_json["printer_type"] = local_machine.second.printer_type;
|
||||
m_json["printer_agent_id"] = local_machine.second.printer_agent_id;
|
||||
m_json["access_code"] = local_machine.second.access_code;
|
||||
|
||||
j["local_machines"][local_machine.first] = m_json;
|
||||
}
|
||||
|
||||
@@ -66,10 +66,19 @@ struct BBLocalMachine
|
||||
std::string dev_ip;
|
||||
std::string dev_id; /* serial number */
|
||||
std::string printer_type; /* model_id */
|
||||
std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */
|
||||
// Access code, scoped to printer_agent_id above - so a code saved while bound under one
|
||||
// printer agent isn't treated as valid for a different, independent agent talking to the
|
||||
// same physical dev_id. Empty for entries persisted before this field existed; those fall
|
||||
// back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only,
|
||||
// since BBL was the only agent when they were saved) - see
|
||||
// get_access_code_with_legacy_fallback() in DevManager.cpp.
|
||||
std::string access_code;
|
||||
|
||||
bool operator==(const BBLocalMachine& other) const
|
||||
{
|
||||
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type;
|
||||
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type &&
|
||||
printer_agent_id == other.printer_agent_id && access_code == other.access_code;
|
||||
}
|
||||
bool operator!=(const BBLocalMachine& other) const { return !operator==(other); }
|
||||
};
|
||||
|
||||
@@ -154,8 +154,8 @@ void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const
|
||||
//h^2 = L^2 / b^2 [factor the divisor]
|
||||
const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2);
|
||||
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
|
||||
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) //Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current, previous, next) <= scaled<double>(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) //Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current, previous, next) <= double(colinear_vertex_tolerance()))) // make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
continue;
|
||||
|
||||
if (length2 < smallest_line_segment_squared
|
||||
|
||||
@@ -133,8 +133,8 @@ void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const
|
||||
const auto height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2));
|
||||
const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next);
|
||||
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
|
||||
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) // Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= scaled<double>(0.005)) // Make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) // Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= double(colinear_vertex_tolerance())) // Make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
// We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed
|
||||
&& extrusion_area_error <= maximum_extrusion_area_deviation)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,14 @@ class Flow;
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
// ORCA: Tolerance of the "almost exactly colinear" early-out shared by the two simplify() passes
|
||||
// (this file and WallToolPaths.cpp). That test drops a vertex regardless of the user's Maximum wall
|
||||
// resolution/deviation, so it has to stay at the scale of coordinate rounding noise. A larger value
|
||||
// silently decimates finely tessellated curves: on a circle, one vertex may be removed whenever the
|
||||
// sagitta of the resulting chord falls below the tolerance, which halves the point count and turns
|
||||
// smooth arcs into corners the firmware has to decelerate through.
|
||||
inline coord_t colinear_vertex_tolerance() { return coord_t(SCALED_EPSILON); }
|
||||
|
||||
/*!
|
||||
* Represents a polyline (not just a line) that is to be extruded with variable
|
||||
* line width.
|
||||
|
||||
@@ -57,24 +57,24 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData
|
||||
//BBS: can be fit as arc, then save arc data temperarily
|
||||
last_arc = target_arc;
|
||||
if (back_index == points.size() - 1) {
|
||||
result.emplace_back(std::move(PathFittingData{ front_index,
|
||||
result.emplace_back(PathFittingData{ front_index,
|
||||
back_index,
|
||||
last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw,
|
||||
last_arc }));
|
||||
last_arc });
|
||||
front_index = back_index;
|
||||
}
|
||||
} else {
|
||||
if (back_index - front_index > 2) {
|
||||
//BBS: althought current point_stack can't be fit as arc,
|
||||
//but previous must can be fit if removing the top in stack, so save last arc
|
||||
result.emplace_back(std::move(PathFittingData{ front_index,
|
||||
result.emplace_back(PathFittingData{ front_index,
|
||||
back_index - 1,
|
||||
last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw,
|
||||
last_arc }));
|
||||
last_arc });
|
||||
} else {
|
||||
//BBS: save the first segment as line move when 3 point-line can't be fit as arc move
|
||||
if (result.empty() || result.back().path_type != EMovePathType::Linear_move)
|
||||
result.emplace_back(std::move(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()}));
|
||||
result.emplace_back(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()});
|
||||
else if(result.back().path_type == EMovePathType::Linear_move)
|
||||
result.back().end_point_index = front_index + 1;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData
|
||||
//BBS: handle the remain data
|
||||
if (front_index != back_index) {
|
||||
if (result.empty() || result.back().path_type != EMovePathType::Linear_move)
|
||||
result.emplace_back(std::move(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()}));
|
||||
result.emplace_back(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()});
|
||||
else if (result.back().path_type == EMovePathType::Linear_move)
|
||||
result.back().end_point_index = back_index;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -348,6 +362,8 @@ set(lisbslic3r_sources
|
||||
Polyline.hpp
|
||||
PresetBundle.cpp
|
||||
PresetBundle.hpp
|
||||
PresetCacheFormat.cpp
|
||||
PresetCacheFormat.hpp
|
||||
Preset.cpp
|
||||
Preset.hpp
|
||||
PrincipalComponents2D.cpp
|
||||
@@ -509,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)
|
||||
|
||||
@@ -549,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
|
||||
@@ -597,6 +614,7 @@ target_link_libraries(libslic3r
|
||||
libnest2d
|
||||
miniz
|
||||
opencv_world
|
||||
assimp::assimp
|
||||
PRIVATE
|
||||
${CMAKE_DL_LIBS}
|
||||
${EXPAT_LIBRARIES}
|
||||
|
||||
@@ -13,8 +13,8 @@ Slic3r::Polylines Paths64_to_polylines(const Clipper2Lib::Paths64& in)
|
||||
Slic3r::Points points;
|
||||
points.reserve(path64.size());
|
||||
for (const Clipper2Lib::Point64& point64 : path64)
|
||||
points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y)));
|
||||
out.emplace_back(std::move(Slic3r::Polyline(points)));
|
||||
points.emplace_back(Slic3r::Point(point64.x, point64.y));
|
||||
out.emplace_back(Slic3r::Polyline(points));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ Clipper2Lib::Paths64 Slic3rPoints_to_Paths64(const Container& in)
|
||||
Clipper2Lib::Path64 path;
|
||||
path.reserve(item.size());
|
||||
for (const Slic3r::Point& point : item.points)
|
||||
path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
|
||||
path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
|
||||
out.emplace_back(std::move(path));
|
||||
}
|
||||
return out;
|
||||
@@ -44,7 +44,7 @@ Points Path64ToPoints(const Clipper2Lib::Path64& path64)
|
||||
{
|
||||
Points points;
|
||||
points.reserve(path64.size());
|
||||
for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y)));
|
||||
for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(Slic3r::Point(point64.x, point64.y));
|
||||
return points;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ Clipper2Lib::Paths64 Slic3rPolygons_to_Paths64(const Polygons &in)
|
||||
for (const Polygon &poly : in) {
|
||||
Clipper2Lib::Path64 path;
|
||||
path.reserve(poly.points.size());
|
||||
for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
|
||||
for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
|
||||
out.emplace_back(std::move(path));
|
||||
}
|
||||
return out;
|
||||
@@ -114,7 +114,7 @@ Clipper2Lib::Paths64 Slic3rExPolygons_to_Paths64(const ExPolygons& in)
|
||||
const auto &poly = expolygon.contour_or_hole(i);
|
||||
Clipper2Lib::Path64 path;
|
||||
path.reserve(poly.points.size());
|
||||
for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
|
||||
for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
|
||||
out.emplace_back(std::move(path));
|
||||
}
|
||||
}
|
||||
@@ -134,8 +134,8 @@ Polylines _clipper2_pl_open(Clipper2Lib::ClipType clipType, const Slic3r::Polyli
|
||||
|
||||
Slic3r::Polylines out;
|
||||
out.reserve(solution.size() + solution_open.size());
|
||||
polylines_append(out, std::move(Paths64_to_polylines(solution)));
|
||||
polylines_append(out, std::move(Paths64_to_polylines(solution_open)));
|
||||
polylines_append(out, Paths64_to_polylines(solution));
|
||||
polylines_append(out, Paths64_to_polylines(solution_open));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -2031,7 +2031,8 @@ const double& DynamicConfig::opt_float(const t_config_option_key &opt_key, unsig
|
||||
return opt_floats_nullable->get_at(idx);
|
||||
} else {
|
||||
assert(false);
|
||||
return 0;
|
||||
static const double zero = 0.0;
|
||||
return zero;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
#include <cereal/access.hpp>
|
||||
#include <cereal/types/base_class.hpp>
|
||||
// The serialize() members below archive ConfigOption hierarchies through
|
||||
// cereal::base_class, whose registration machinery lives in polymorphic.hpp.
|
||||
#include <cereal/types/polymorphic.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
struct FloatOrPercent
|
||||
|
||||
@@ -682,7 +682,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
|
||||
return fuzzified;
|
||||
}
|
||||
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour)
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed)
|
||||
{
|
||||
const auto slice_z = perimeter_generator.slice_z;
|
||||
const auto& regions = perimeter_generator.regions_by_fuzzify;
|
||||
@@ -690,7 +690,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
const auto& config = regions.begin()->first;
|
||||
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
|
||||
if (fuzzify)
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, config);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed);
|
||||
} else {
|
||||
// Merge regions that produce identical fuzzy effects (differ only in type).
|
||||
// When the style (e.g. External) and a painted region (All) both fuzzify this loop
|
||||
@@ -701,10 +701,19 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
|
||||
// Fast path: single merged region — apply directly without splitting
|
||||
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open path means this is a thin wall that collapsed into a single thick line, in this case the path will go exactly
|
||||
// between the middle two sides of the object. And since the paint segmentation never goes beyond the middle line because
|
||||
// it uses voronoi diagram, we need to expand the segmentation a little bit to make sure it covers the path.
|
||||
if (!closed) {
|
||||
for (auto& r : merged_regions) {
|
||||
r.expolygons = offset_ex(r.expolygons, perimeter_generator.ext_perimeter_flow.scaled_width() / 10);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FUZZY
|
||||
{
|
||||
int i = 0;
|
||||
@@ -752,7 +761,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
// Fuzzy splitted extrusion
|
||||
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
|
||||
// The entire polygon is fuzzified
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed);
|
||||
continue;
|
||||
} else {
|
||||
const auto current_ext = extrusion->junctions;
|
||||
@@ -803,7 +812,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
}
|
||||
|
||||
//Orca: ensure the loop is closed after fuzzy
|
||||
if (!extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
|
||||
if (closed && !extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
|
||||
extrusion->junctions.back().p = extrusion->junctions.front().p;
|
||||
extrusion->junctions.back().w = extrusion->junctions.front().w;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ void group_region_by_fuzzify(PerimeterGenerator& g);
|
||||
bool should_fuzzify(const FuzzySkinConfig& config, int layer_id, size_t loop_idx, bool is_contour);
|
||||
|
||||
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, size_t loop_idx, bool is_contour);
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour);
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour, bool closed = true);
|
||||
|
||||
} // namespace Slic3r::Feature::FuzzySkin
|
||||
|
||||
|
||||
@@ -1021,7 +1021,7 @@ namespace Slic3r
|
||||
if (FGMode::MatchMode == ctx.group_info.mode)
|
||||
return calc_filament_group_for_match(cost);
|
||||
}
|
||||
catch (const FilamentGroupException& e) {
|
||||
catch (const FilamentGroupException&) {
|
||||
}
|
||||
|
||||
return calc_filament_group_for_flush(cost);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -351,19 +351,23 @@ void Node::convertToPolylines(Polylines &output, const coord_t line_overlap) con
|
||||
{
|
||||
Polylines result;
|
||||
result.emplace_back();
|
||||
convertToPolylines(0, result);
|
||||
// Orca: the layers are filled in parallel, so they would consume a shared generator in a
|
||||
// different order every run, and a model would not slice the same way twice. Each tree seeds
|
||||
// its own from where it is rooted; one constant seed would start them all on the same pick.
|
||||
std::mt19937_64 rng { uint64_t(PointHash{}(m_p)) };
|
||||
convertToPolylines(0, result, rng);
|
||||
removeJunctionOverlap(result, line_overlap);
|
||||
append(output, std::move(result));
|
||||
}
|
||||
|
||||
void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
|
||||
void Node::convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const
|
||||
{
|
||||
if (m_children.empty()) {
|
||||
output[long_line_idx].points.push_back(m_p);
|
||||
return;
|
||||
}
|
||||
size_t first_child_idx = rand() % m_children.size();
|
||||
m_children[first_child_idx]->convertToPolylines(long_line_idx, output);
|
||||
const size_t first_child_idx = rng() % m_children.size();
|
||||
m_children[first_child_idx]->convertToPolylines(long_line_idx, output, rng);
|
||||
output[long_line_idx].points.push_back(m_p);
|
||||
|
||||
for (size_t idx_offset = 1; idx_offset < m_children.size(); idx_offset++) {
|
||||
@@ -371,7 +375,7 @@ void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
|
||||
const Node& child = *m_children[child_idx];
|
||||
output.emplace_back();
|
||||
size_t child_line_idx = output.size() - 1;
|
||||
child.convertToPolylines(child_line_idx, output);
|
||||
child.convertToPolylines(child_line_idx, output, rng);
|
||||
output[child_line_idx].points.emplace_back(m_p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "../../EdgeGrid.hpp"
|
||||
@@ -259,8 +260,9 @@ protected:
|
||||
*
|
||||
* \param long_line a reference to a polyline in \p output which to continue building on in the recursion
|
||||
* \param output all branches in this tree connected into polylines
|
||||
* \param rng the generator the junctions draw from, carried through the recursion
|
||||
*/
|
||||
void convertToPolylines(size_t long_line_idx, Polylines &output) const;
|
||||
void convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const;
|
||||
|
||||
void removeJunctionOverlap(Polylines &polylines, coord_t line_overlap) const;
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -21,7 +23,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message)
|
||||
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl)
|
||||
{
|
||||
if (meshptr == nullptr)
|
||||
return false;
|
||||
@@ -53,9 +55,9 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
boost::filesystem::path temp_mtl_path(mtl_file);
|
||||
mtl_path = temp_mtl_path;
|
||||
}
|
||||
auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str();
|
||||
const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string();
|
||||
if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) {
|
||||
if (!ObjParser::mtlparse(_mtl_path, mtl_data)) {
|
||||
if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path;
|
||||
message = _L("load mtl in obj: failed to parse");
|
||||
return false;
|
||||
@@ -98,6 +100,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
its.indices.reserve(num_faces + num_quads);
|
||||
if (exist_mtl) {
|
||||
obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1;
|
||||
obj_info.usemtls = data.usemtls;
|
||||
obj_info.face_colors.reserve(num_faces + num_quads);
|
||||
}
|
||||
bool has_color = data.has_vertex_color;
|
||||
@@ -210,14 +213,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
}
|
||||
if (meshptr->volume() < 0)
|
||||
meshptr->flip_triangles();
|
||||
// Hand the parsed material table back so callers can build a TexturedMesh from it.
|
||||
if (out_mtl)
|
||||
*out_mtl = mtl_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in)
|
||||
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl)
|
||||
{
|
||||
TriangleMesh mesh;
|
||||
|
||||
bool ret = load_obj(path, &mesh, obj_info, message);
|
||||
bool ret = load_obj(path, &mesh, obj_info, message, out_mtl);
|
||||
|
||||
if (ret) {
|
||||
std::string object_name;
|
||||
@@ -232,6 +238,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool obj_to_textured_mesh(
|
||||
const ObjInfo& obj_info,
|
||||
const indexed_triangle_set& its,
|
||||
const ObjParser::MtlData& mtl_data,
|
||||
const std::string& obj_directory,
|
||||
TexturedMesh& out)
|
||||
{
|
||||
if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png)
|
||||
return false;
|
||||
|
||||
const size_t nv = its.vertices.size();
|
||||
const size_t nf = its.indices.size();
|
||||
|
||||
// 1. Copy vertices
|
||||
out.vertices.resize(nv);
|
||||
for (size_t i = 0; i < nv; ++i)
|
||||
out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
|
||||
|
||||
// 2. Copy face indices
|
||||
out.indices.resize(nf);
|
||||
for (size_t i = 0; i < nf; ++i)
|
||||
out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
|
||||
|
||||
// 3. Build per-face UV (uv_coords + uv_indices)
|
||||
// OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV).
|
||||
// Flip V here so downstream code works uniformly.
|
||||
if (!obj_info.uvs.empty()) {
|
||||
const size_t uv_face_count = obj_info.uvs.size();
|
||||
out.uv_coords.resize(uv_face_count * 3);
|
||||
out.uv_indices.resize(nf);
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
if (fi < uv_face_count) {
|
||||
int base = static_cast<int>(fi * 3);
|
||||
out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()};
|
||||
out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()};
|
||||
out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()};
|
||||
out.uv_indices[fi] = {base, base + 1, base + 2};
|
||||
} else {
|
||||
out.uv_indices[fi] = {0, 0, 0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Build material list and load textures from disk
|
||||
// Map: material name -> material index
|
||||
std::map<std::string, int> mtl_name_to_idx;
|
||||
for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i)
|
||||
mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i);
|
||||
|
||||
const int num_materials = static_cast<int>(mtl_data.mtl_orders.size());
|
||||
out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f});
|
||||
out.material_texture_map.resize(num_materials, -1);
|
||||
|
||||
// Map: texture filename -> index in out.textures
|
||||
std::map<std::string, int> png_to_tex_idx;
|
||||
|
||||
for (int mi = 0; mi < num_materials; ++mi) {
|
||||
const std::string& name = mtl_data.mtl_orders[mi];
|
||||
auto it = mtl_data.new_mtl_unmap.find(name);
|
||||
if (it == mtl_data.new_mtl_unmap.end())
|
||||
continue;
|
||||
const auto& mtl = *(it->second);
|
||||
|
||||
// Material color from Kd
|
||||
out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr};
|
||||
|
||||
// Texture from map_Kd
|
||||
if (mtl.map_Kd.empty())
|
||||
continue;
|
||||
|
||||
auto tex_it = png_to_tex_idx.find(mtl.map_Kd);
|
||||
if (tex_it != png_to_tex_idx.end()) {
|
||||
out.material_texture_map[mi] = tex_it->second;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve texture file path.
|
||||
const boost::filesystem::path requested_tex_path(mtl.map_Kd);
|
||||
const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ?
|
||||
resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") :
|
||||
resource_path::resolve_existing_relative_path_case_insensitive(
|
||||
boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd");
|
||||
|
||||
if (tex_path.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read raw file bytes
|
||||
boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open())
|
||||
continue;
|
||||
auto file_size = file.tellg();
|
||||
if (file_size <= 0)
|
||||
continue;
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
TextureImage ti;
|
||||
ti.data.resize(static_cast<size_t>(file_size));
|
||||
file.read(reinterpret_cast<char*>(ti.data.data()), file_size);
|
||||
ti.width = -1;
|
||||
ti.height = -1;
|
||||
ti.channels = 0;
|
||||
|
||||
int new_idx = static_cast<int>(out.textures.size());
|
||||
out.textures.push_back(std::move(ti));
|
||||
png_to_tex_idx[mtl.map_Kd] = new_idx;
|
||||
out.material_texture_map[mi] = new_idx;
|
||||
}
|
||||
|
||||
// 5. Build per-face material_ids from usemtls ranges
|
||||
out.material_ids.resize(nf, -1);
|
||||
if (!obj_info.usemtls.empty()) {
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
int face_idx = static_cast<int>(fi);
|
||||
for (size_t k = 0; k < obj_info.usemtls.size(); ++k) {
|
||||
const auto& um = obj_info.usemtls[k];
|
||||
if (face_idx >= um.face_start && face_idx <= um.face_end) {
|
||||
auto name_it = mtl_name_to_idx.find(um.name);
|
||||
if (name_it != mtl_name_to_idx.end())
|
||||
out.material_ids[fi] = name_it->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (out.textures.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded";
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, "
|
||||
<< out.textures.size() << " textures, "
|
||||
<< num_materials << " materials";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool store_obj(const char *path, TriangleMesh *mesh)
|
||||
{
|
||||
//FIXME returning false even if write failed.
|
||||
|
||||
@@ -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_ */
|
||||
@@ -111,14 +111,19 @@ bool StepPreProcessor::isUtf8File(const char* path)
|
||||
bool StepPreProcessor::isUtf8(const std::string str)
|
||||
{
|
||||
size_t num = 0;
|
||||
int i = 0;
|
||||
size_t i = 0;
|
||||
while (i < str.length()) {
|
||||
if ((str[i] & 0x80) == 0x00) {
|
||||
const unsigned char lead = static_cast<unsigned char>(str[i]);
|
||||
if ((lead & 0x80) == 0x00) {
|
||||
i++;
|
||||
} else if ((num = preNum(str[i])) > 2) {
|
||||
// preNum() counts the leading 1 bits, and a multi-byte sequence is 2 to 4
|
||||
// bytes long, so anything outside that range is not a lead byte.
|
||||
} else if ((num = preNum(lead)) >= 2 && num <= 4) {
|
||||
if (i + num > str.length())
|
||||
return false;
|
||||
i++;
|
||||
for (int j = 0; j < num - 1; j++) {
|
||||
if ((str[i] & 0xc0) != 0x80)
|
||||
for (size_t j = 0; j < num - 1; j++) {
|
||||
if ((static_cast<unsigned char>(str[i]) & 0xc0) != 0x80)
|
||||
return false;
|
||||
i++;
|
||||
}
|
||||
@@ -132,15 +137,20 @@ bool StepPreProcessor::isUtf8(const std::string str)
|
||||
bool StepPreProcessor::isGBK(const std::string str) {
|
||||
size_t i = 0;
|
||||
while (i < str.length()) {
|
||||
if (str[i] <= 0x7f) {
|
||||
// char is signed here, so every byte compares <= 0x7f unless widened first.
|
||||
const unsigned char lead = static_cast<unsigned char>(str[i]);
|
||||
if (lead <= 0x7f) {
|
||||
i++;
|
||||
continue;
|
||||
} else {
|
||||
if (str[i] >= 0x81 &&
|
||||
str[i] <= 0xfe &&
|
||||
str[i + 1] >= 0x40 &&
|
||||
str[i + 1] <= 0xfe &&
|
||||
str[i + 1] != 0xf7) {
|
||||
if (i + 1 >= str.length())
|
||||
return false;
|
||||
const unsigned char trail = static_cast<unsigned char>(str[i + 1]);
|
||||
if (lead >= 0x81 &&
|
||||
lead <= 0xfe &&
|
||||
trail >= 0x40 &&
|
||||
trail <= 0xfe &&
|
||||
trail != 0xf7) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
@@ -586,7 +596,7 @@ Step::Step_Status Step::mesh(Model* model,
|
||||
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
|
||||
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
|
||||
aPnt.Transform(aTrsf);
|
||||
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
|
||||
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
|
||||
}
|
||||
// BBS: copy triangles
|
||||
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
|
||||
@@ -712,7 +722,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} catch(const Exception &e) {
|
||||
} catch(const Exception &) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -1315,6 +1318,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);
|
||||
@@ -2694,6 +2698,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3511,6 +3523,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)
|
||||
@@ -4684,6 +4698,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
|
||||
@@ -8488,6 +8519,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";
|
||||
}
|
||||
@@ -8921,7 +8963,7 @@ private:
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inital and interval = " << m_interval;
|
||||
m_next_backup = boost::get_system_time() + boost::posix_time::seconds(m_interval);
|
||||
boost::unique_lock lock(m_mutex);
|
||||
m_thread = std::move(boost::thread(boost::ref(*this)));
|
||||
m_thread = boost::thread(boost::ref(*this));
|
||||
}
|
||||
|
||||
~_BBS_Backup_Manager() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -352,7 +352,7 @@ bool load_svg(const char *path, Model *model, std::string &message)
|
||||
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
|
||||
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
|
||||
aPnt.Transform(aTrsf);
|
||||
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
|
||||
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
|
||||
}
|
||||
// BBS: copy triangles
|
||||
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
|
||||
|
||||
+362
-9
@@ -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;
|
||||
}
|
||||
@@ -8726,7 +9079,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp
|
||||
continue;
|
||||
|
||||
Polygons temp;
|
||||
temp.emplace_back(std::move(instance_bbox.polygon()));
|
||||
temp.emplace_back(instance_bbox.polygon());
|
||||
if (intersection_pl(travel, temp).empty())
|
||||
continue;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset()
|
||||
//BBS
|
||||
enter_direction = { 0.0f, 0.0f, 0.0f };
|
||||
exit_direction = { 0.0f, 0.0f, 0.0f };
|
||||
jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
}
|
||||
|
||||
void GCodeProcessor::TimeMachine::CustomGCodeTime::reset()
|
||||
@@ -2542,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();
|
||||
@@ -5036,6 +5038,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
if (!is_extrusion_only_move(delta_pos))
|
||||
curr.enter_direction = curr.enter_direction / norm;
|
||||
curr.exit_direction = curr.enter_direction;
|
||||
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
|
||||
static_cast<float>(delta_pos[Y]),
|
||||
static_cast<float>(delta_pos[Z]),
|
||||
static_cast<float>(delta_pos[E])).normalized();
|
||||
|
||||
TimeBlock block;
|
||||
block.move_type = type;
|
||||
@@ -5118,22 +5124,32 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
|
||||
block.acceleration = acceleration;
|
||||
|
||||
// calculates block exit feedrate
|
||||
curr.safe_feedrate = block.feedrate_profile.cruise;
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
|
||||
// Negative leaves the classic jerk path below unchanged.
|
||||
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
|
||||
static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
||||
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
|
||||
|
||||
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
|
||||
// free to start from rest.
|
||||
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
|
||||
|
||||
if (!use_junction_deviation) {
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
}
|
||||
}
|
||||
|
||||
block.feedrate_profile.exit = curr.safe_feedrate;
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
|
||||
// calculates block entry feedrate
|
||||
float vmax_junction = curr.safe_feedrate;
|
||||
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
|
||||
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
|
||||
if (!use_junction_deviation && has_prev_move) {
|
||||
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
|
||||
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
|
||||
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
|
||||
@@ -5400,6 +5416,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
|
||||
if (!is_extrusion_only_move(delta_pos))
|
||||
curr.enter_direction = curr.enter_direction / norm;
|
||||
curr.exit_direction = curr.enter_direction;
|
||||
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
|
||||
static_cast<float>(delta_pos[Y]),
|
||||
static_cast<float>(delta_pos[Z]),
|
||||
static_cast<float>(delta_pos[E])).normalized();
|
||||
|
||||
TimeBlock block;
|
||||
block.move_type = type;
|
||||
@@ -5480,22 +5500,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
|
||||
|
||||
block.acceleration = acceleration;
|
||||
|
||||
// calculates block exit feedrate
|
||||
curr.safe_feedrate = block.feedrate_profile.cruise;
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
|
||||
// Negative leaves the classic jerk path below unchanged.
|
||||
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
|
||||
static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
||||
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
|
||||
|
||||
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
|
||||
// free to start from rest.
|
||||
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
|
||||
|
||||
if (!use_junction_deviation) {
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
}
|
||||
}
|
||||
|
||||
block.feedrate_profile.exit = curr.safe_feedrate;
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
|
||||
// calculates block entry feedrate
|
||||
float vmax_junction = curr.safe_feedrate;
|
||||
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
|
||||
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
|
||||
if (!use_junction_deviation && has_prev_move) {
|
||||
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
|
||||
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
|
||||
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
|
||||
@@ -7168,6 +7198,91 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM
|
||||
return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode));
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
|
||||
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
|
||||
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
|
||||
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
|
||||
if (scv <= 0.0f || acceleration <= 0.0f)
|
||||
return 0.0f;
|
||||
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
|
||||
}
|
||||
|
||||
// Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0.
|
||||
if (m_flavor == gcfMarlinFirmware)
|
||||
return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id);
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
float junction_acceleration = block.acceleration;
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
if (junction_unit_vec[a] == 0.0f)
|
||||
continue;
|
||||
const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast<Axis>(a), m_machine_config_idx);
|
||||
if (axis_max_acceleration > 0.0f)
|
||||
junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a]));
|
||||
}
|
||||
return junction_acceleration;
|
||||
}
|
||||
|
||||
// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp).
|
||||
float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
|
||||
const TimeMachine::State& curr, bool has_prev_move,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
|
||||
if (junction_deviation <= 0.0f)
|
||||
return -1.0f; // classic jerk machine, the caller keeps its own computation
|
||||
if (!has_prev_move)
|
||||
return 0.0f; // starts from rest, the planner raises this on the reverse pass
|
||||
|
||||
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
|
||||
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
|
||||
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
|
||||
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
|
||||
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
|
||||
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
|
||||
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
|
||||
if (junction_cos_theta > 0.999999f)
|
||||
return 0.0f; // the path doubles back, the machine has to stop
|
||||
junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below
|
||||
|
||||
const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive
|
||||
const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec;
|
||||
const float junction_vec_norm = junction_vec.norm();
|
||||
const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm)
|
||||
: Vec4f(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode);
|
||||
|
||||
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
|
||||
|
||||
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
|
||||
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
|
||||
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
||||
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
|
||||
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
|
||||
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
|
||||
const float t = neg * junction_cos_theta;
|
||||
const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f +
|
||||
t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f))))));
|
||||
const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033
|
||||
vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta);
|
||||
}
|
||||
|
||||
// Never faster than either of the two moves the junction joins.
|
||||
vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate)));
|
||||
return std::sqrt(vmax_junction_sqr);
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
@@ -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;
|
||||
@@ -637,6 +641,9 @@ class Print;
|
||||
//For line move, there are same. For arc move, there are different.
|
||||
Vec3f enter_direction;
|
||||
Vec3f exit_direction;
|
||||
// Orca: move direction over all four axes, unit length. Used by
|
||||
// calc_vmax_junction_deviation(); see there for why E is normalized in.
|
||||
Vec4f jd_unit_vec;
|
||||
|
||||
void reset();
|
||||
};
|
||||
@@ -1488,6 +1495,16 @@ class Print;
|
||||
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
|
||||
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const;
|
||||
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
|
||||
// Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine.
|
||||
float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const;
|
||||
// Orca: acceleration along the junction direction, clamped by the per axis limits.
|
||||
float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
// Orca: entry speed from the junction deviation model, which limits a corner by its angle alone
|
||||
// and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead.
|
||||
float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
|
||||
const TimeMachine::State& curr, bool has_prev_move,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
|
||||
Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
|
||||
@@ -32,7 +32,7 @@ using ThumbnailsList = std::vector<ThumbnailData>;
|
||||
|
||||
struct ThumbnailsParams
|
||||
{
|
||||
const Vec2ds sizes;
|
||||
const Vec2ds sizes{};
|
||||
bool printable_only;
|
||||
bool parts_only;
|
||||
bool show_bed;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -502,7 +502,7 @@ void segment(CGALMesh& src, std::vector<CGALMesh>& dst, double smoothing_alpha =
|
||||
//}
|
||||
//else
|
||||
{
|
||||
dst.emplace_back(std::move(CGALMesh(out)));
|
||||
dst.emplace_back(CGALMesh(out));
|
||||
}
|
||||
}
|
||||
//if (mesh_merged.is_empty() == false) {
|
||||
@@ -521,7 +521,7 @@ std::vector<TriangleMesh> segment(const TriangleMesh& src, double smoothing_alph
|
||||
std::vector<TriangleMesh> out_meshes;
|
||||
for (auto& outf_cgal_mesh: out_cgal_meshes)
|
||||
{
|
||||
out_meshes.emplace_back(std::move(cgal_to_triangle_mesh(outf_cgal_mesh.m)));
|
||||
out_meshes.emplace_back(cgal_to_triangle_mesh(outf_cgal_mesh.m));
|
||||
}
|
||||
|
||||
return out_meshes;
|
||||
|
||||
+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(), 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
|
||||
@@ -2581,7 +2660,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) {
|
||||
@@ -2592,8 +2672,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3500,6 +3594,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
@@ -49,6 +49,8 @@ namespace cereal {
|
||||
}
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct TexturedMesh;
|
||||
enum class ConversionType;
|
||||
|
||||
class BuildVolume;
|
||||
@@ -742,6 +744,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(); }
|
||||
@@ -997,7 +1002,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.
|
||||
@@ -1645,6 +1651,10 @@ public:
|
||||
std::shared_ptr<ModelInfo> model_info = nullptr;
|
||||
std::shared_ptr<ModelProfileInfo> profile_info = nullptr;
|
||||
|
||||
// Textured mesh data for texture-to-painting import. Populated by the loader when a mesh
|
||||
// arrives with usable UVs and a texture map; consumed (and reset) by the import dialog.
|
||||
std::shared_ptr<TexturedMesh> texture_mesh;
|
||||
|
||||
//makerlab information
|
||||
std::string mk_name;
|
||||
std::string mk_version;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include "OpenVDBUtils.hpp"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
|
||||
@@ -229,6 +229,22 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
|
||||
|
||||
// Append thin walls to the nearest-neighbor search (only for first iteration)
|
||||
if (! thin_walls.empty()) {
|
||||
// Orca: apply fuzzy skin to thin walls as well
|
||||
for (auto& thin_wall : thin_walls) {
|
||||
// First, we convert the ThickPolyline into Arachne::ExtrusionLine so we could reuse our existing fuzzy code
|
||||
Arachne::ExtrusionLine el(0, true);
|
||||
el.junctions.reserve(thin_wall.points.size());
|
||||
for (int i = 0; i < thin_wall.points.size(); i++) {
|
||||
el.junctions.emplace_back(thin_wall.points[i], thin_wall.width[i], 0);
|
||||
}
|
||||
|
||||
// Then we fuzzy it
|
||||
apply_fuzzy_skin(&el, perimeter_generator, true, thin_wall.is_closed());
|
||||
|
||||
// Then convert the result back to ThickPolyline
|
||||
thin_wall = Arachne::to_thick_polyline(el);
|
||||
}
|
||||
|
||||
variable_width(thin_walls, erExternalPerimeter, perimeter_generator.ext_perimeter_flow, coll.entities);
|
||||
thin_walls.clear();
|
||||
}
|
||||
@@ -392,7 +408,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
||||
ExtrusionRole role = is_external ? erExternalPerimeter : erPerimeter;
|
||||
|
||||
const bool is_contour = !extrusion->is_closed || pg_extrusion.is_contour;
|
||||
apply_fuzzy_skin(extrusion, perimeter_generator, is_contour);
|
||||
apply_fuzzy_skin(extrusion, perimeter_generator, is_contour, extrusion->is_closed);
|
||||
|
||||
ExtrusionPaths paths;
|
||||
// detect overhanging/bridging perimeters
|
||||
|
||||
+27
-11
@@ -8,7 +8,9 @@
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif /* _MSC_VER */
|
||||
|
||||
@@ -147,6 +149,9 @@ Semver get_version_from_json(std::string file_path)
|
||||
return Semver();
|
||||
//throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what()));
|
||||
}
|
||||
catch(...) {
|
||||
return Semver();
|
||||
}
|
||||
}
|
||||
|
||||
//BBS: add a function to load the key-values from xxx.json
|
||||
@@ -261,18 +266,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
|
||||
}
|
||||
};
|
||||
|
||||
// The four variant sets are immutable after static init and probed for every
|
||||
// key of every preset loaded; one merged map makes that a single lookup.
|
||||
// emplace keeps the first insertion, preserving the first-set-wins priority
|
||||
// of the else-if chain this replaces.
|
||||
static const std::unordered_map<std::string, int> variant_class = [] {
|
||||
std::unordered_map<std::string, int> m;
|
||||
for (const std::string& k : print_options_with_variant) m.emplace(k, 0);
|
||||
for (const std::string& k : filament_options_with_variant) m.emplace(k, 1);
|
||||
for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2);
|
||||
for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3);
|
||||
return m;
|
||||
}();
|
||||
|
||||
for(auto& key :config.keys()){
|
||||
if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){
|
||||
replace_nil_and_resize(key, process_variant_length);
|
||||
}
|
||||
else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){
|
||||
replace_nil_and_resize(key, filament_variant_length);
|
||||
}
|
||||
else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){
|
||||
replace_nil_and_resize(key, machine_variant_length);
|
||||
}
|
||||
else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){
|
||||
replace_nil_and_resize(key, machine_variant_length * 2);
|
||||
auto iter = variant_class.find(key);
|
||||
if (iter == variant_class.end())
|
||||
continue;
|
||||
switch (iter->second) {
|
||||
case 0: replace_nil_and_resize(key, process_variant_length); break;
|
||||
case 1: replace_nil_and_resize(key, filament_variant_length); break;
|
||||
case 2: replace_nil_and_resize(key, machine_variant_length); break;
|
||||
case 3: replace_nil_and_resize(key, machine_variant_length * 2); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1170,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",
|
||||
|
||||
@@ -131,6 +131,10 @@ public:
|
||||
PrinterVariant() {}
|
||||
PrinterVariant(const std::string &name) : name(name) {}
|
||||
std::string name;
|
||||
|
||||
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) { ar(name); } // PrinterVariant
|
||||
};
|
||||
|
||||
struct PrinterModel {
|
||||
@@ -139,7 +143,7 @@ public:
|
||||
std::string name;
|
||||
//BBS: this is internal id for the printer. Currently only used for searching in database
|
||||
std::string model_id;
|
||||
PrinterTechnology technology;
|
||||
PrinterTechnology technology = ptFFF;
|
||||
std::string family;
|
||||
std::vector<PrinterVariant> variants;
|
||||
std::vector<std::string> default_materials;
|
||||
@@ -162,6 +166,17 @@ public:
|
||||
}
|
||||
|
||||
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
|
||||
|
||||
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) // PrinterModel
|
||||
{
|
||||
ar(id, name, model_id, technology, family, variants, default_materials,
|
||||
not_support_bed_types, bed_model, bed_texture, image_bed_type,
|
||||
bottom_texture_end_name, use_double_extruder_default_texture,
|
||||
bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect,
|
||||
hotend_model);
|
||||
}
|
||||
};
|
||||
std::vector<PrinterModel> models;
|
||||
|
||||
@@ -173,6 +188,14 @@ public:
|
||||
|
||||
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
|
||||
|
||||
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) // VendorProfile
|
||||
{
|
||||
ar(name, id, config_version, config_update_url, changelog_url,
|
||||
models, default_filaments, default_sla_materials);
|
||||
}
|
||||
|
||||
// Load VendorProfile from an ini file.
|
||||
// If `load_all` is false, only the header with basic info (name, version, URLs) is loaded.
|
||||
static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true);
|
||||
@@ -427,10 +450,10 @@ public:
|
||||
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
||||
|
||||
protected:
|
||||
Preset() = default;
|
||||
|
||||
friend class PresetCollection;
|
||||
friend class PresetBundle;
|
||||
|
||||
Preset() = default;
|
||||
};
|
||||
|
||||
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);
|
||||
|
||||
+819
-389
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,12 @@
|
||||
#define slic3r_PresetBundle_hpp_
|
||||
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "enum_bitmask.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
@@ -170,6 +172,31 @@ struct PresetBundleMetadata
|
||||
class PresetBundle
|
||||
{
|
||||
public:
|
||||
// ---- Per-vendor preset cache --------------------------------------------
|
||||
// One cache file per vendor (plus the Orca filament library), stamped with
|
||||
// the vendor's own profile version rather than a directory scan. The bytes
|
||||
// on disk are VendorCacheFile's business (PresetCacheFormat.hpp); what
|
||||
// lives here is how a cache's contents install into a bundle.
|
||||
|
||||
// The cache is not something a caller loads from: a vendor is loaded with
|
||||
// load_vendor_configs_from_json, which comes from the cache whenever one covers
|
||||
// it. What is public here is what the cache's own tests drive directly.
|
||||
|
||||
// Load a per-vendor cache into this bundle by installing its entries, with
|
||||
// base_bundle's filament library as the inheritance base. Rejects (returns
|
||||
// false, with this bundle left clean) unless VendorCacheFile::load accepts
|
||||
// the file — see its contract for the version and identity checks — and
|
||||
// every entry installs. Options this build no longer defines are dropped,
|
||||
// not fatal — the payload names its own keys.
|
||||
bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name,
|
||||
const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr);
|
||||
|
||||
// Enable writing a per-vendor cache after a JSON parse (off by default). Cache
|
||||
// content is pure parse output, so the guard is policy, not correctness: only
|
||||
// the deliberate generators (load_system_presets_from_json, the cache build
|
||||
// tool) write files, not every incidental load a dialog performs.
|
||||
void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; }
|
||||
|
||||
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
|
||||
Preset &in_print_preset,
|
||||
const DynamicPrintConfig &project_config,
|
||||
@@ -299,8 +326,9 @@ public:
|
||||
// Export selections (current print, current filaments, current printer) into config.ini
|
||||
void export_selections(AppConfig &config);
|
||||
|
||||
// BBS
|
||||
void set_num_filaments(unsigned int n, std::vector<std::string> new_colors);
|
||||
// n is the total slot count, and growth appends at the raw tail - which is where the mixed
|
||||
// slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and
|
||||
// then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does.
|
||||
void set_num_filaments(unsigned int n, std::string new_col = "");
|
||||
void update_num_filaments(unsigned int to_del_flament_id);
|
||||
|
||||
@@ -444,8 +472,12 @@ public:
|
||||
/*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle(
|
||||
const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
|
||||
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
|
||||
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
|
||||
// not the profile JSONs are still there. A whole-vendor load comes from the
|
||||
// vendor's preset cache whenever one covers the profile on disk, and is parsed
|
||||
// from the JSONs in `dir` only when none does. Nothing here reads resources.
|
||||
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
|
||||
const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
|
||||
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
|
||||
|
||||
// Export a config bundle file containing all the presets and the names of the active presets.
|
||||
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
|
||||
@@ -466,6 +498,14 @@ 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;
|
||||
// How many slots hold a real filament, i.e. everything ahead of the mixed tail.
|
||||
size_t num_physical_filaments() const;
|
||||
|
||||
void on_extruders_count_changed(int extruder_count);
|
||||
|
||||
@@ -517,11 +557,49 @@ public:
|
||||
// Orca: for validation only.
|
||||
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
|
||||
|
||||
// Errors the last load recorded. What the cache's error accounting promises —
|
||||
// a cache-served vendor reports what its parse would — is pinned against this.
|
||||
int error_count() const { return m_errors; }
|
||||
|
||||
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
|
||||
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
|
||||
bool check_preset_references() const;
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
||||
|
||||
private:
|
||||
// Load one vendor from the preset cache installed in `dir`, judged against
|
||||
// the vendor profile there. False, with this bundle left clean, when there
|
||||
// is no usable cache and the vendor has to be parsed. This is how
|
||||
// load_vendor_configs_from_json reads a cache.
|
||||
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
|
||||
|
||||
// Load one source-form preset entry into this bundle: resolve `inherits`,
|
||||
// flatten, validate and register the preset. Returns the reason loading
|
||||
// failed, empty on success. See the definition for the sharing contract
|
||||
// between the JSON parse and the cache load.
|
||||
// retain_configs, when non-null, names the only presets registered into
|
||||
// config_maps (a full config copy each). The cache load passes the names its
|
||||
// entries inherit — the only ones ever looked up again; the JSON parse
|
||||
// retains all, not knowing what later subfiles inherit.
|
||||
std::string load_vendor_preset(const CachedPreset& entry,
|
||||
const std::string& path, const std::string& vendor_name,
|
||||
const PresetBundle* base_bundle,
|
||||
LoadConfigBundleAttributes flags,
|
||||
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
|
||||
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
|
||||
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
|
||||
const std::set<std::string>* retain_configs = nullptr);
|
||||
|
||||
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
||||
void clear_printer_hold_aliases();
|
||||
|
||||
// Whether to (re)write a per-vendor cache after a JSON parse.
|
||||
bool m_generate_vendor_caches { false };
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
bool check_duplicate_filament_subtypes() const;
|
||||
@@ -529,8 +607,6 @@ private:
|
||||
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
//BBS: add json related logic
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
||||
// Update the multicolor information for filaments.
|
||||
void update_filament_multi_color();
|
||||
// Update renamed_from and alias maps of system profiles.
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
#include "libslic3r/PresetCacheFormat.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/crc.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/iostreams/device/array.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cereal/types/map.hpp>
|
||||
#include <cereal/types/set.hpp>
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
CacheDictionary::CacheDictionary()
|
||||
{
|
||||
// ENUM_UNNAMED is index 0 and always the empty name.
|
||||
m_enum_values.emplace_back();
|
||||
}
|
||||
|
||||
// The ints an enum option holds — one for a coEnum, the whole vector for coEnums.
|
||||
static std::vector<int> enum_ints(const ConfigOptionDef& def, const ConfigOption* opt)
|
||||
{
|
||||
if (def.type == coEnum)
|
||||
return { opt->getInt() };
|
||||
return static_cast<const ConfigOptionInts*>(opt)->values;
|
||||
}
|
||||
|
||||
// The name this build gives one of those ints, empty where it has none — a
|
||||
// nullable option's nil, or a definition carrying no enum_keys_map. Enums are
|
||||
// written by name so a build that reorders an enum's values still reads it right.
|
||||
static std::string enum_name_of(const ConfigOptionDef& def, int value)
|
||||
{
|
||||
if (def.enum_keys_map != nullptr)
|
||||
for (const auto& kvp : *def.enum_keys_map)
|
||||
if (kvp.second == value)
|
||||
return kvp.first;
|
||||
return {};
|
||||
}
|
||||
|
||||
void CacheDictionary::collect(const DynamicPrintConfig& config)
|
||||
{
|
||||
for (auto it = config.cbegin(); it != config.cend(); ++ it) {
|
||||
const ConfigOptionDef* def = print_config_def.get(it->first);
|
||||
if (def == nullptr)
|
||||
continue; // save_config does not write it either
|
||||
if (m_key_index.try_emplace(it->first, uint16_t(m_keys.size())).second) {
|
||||
m_keys.push_back(it->first);
|
||||
m_types.push_back(uint16_t(def->type));
|
||||
}
|
||||
if (def->type != coEnum && def->type != coEnums)
|
||||
continue;
|
||||
for (int value : enum_ints(*def, it->second.get())) {
|
||||
std::string name = enum_name_of(*def, value);
|
||||
if (! name.empty() && m_enum_index.try_emplace(name, uint16_t(m_enum_values.size())).second)
|
||||
m_enum_values.push_back(std::move(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t CacheDictionary::key_index(const t_config_option_key& key) const
|
||||
{
|
||||
auto it = m_key_index.find(key);
|
||||
if (it == m_key_index.end())
|
||||
throw std::runtime_error("preset cache: option " + key + " was never collected into the dictionary");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
uint16_t CacheDictionary::enum_index(const std::string& name) const
|
||||
{
|
||||
if (name.empty())
|
||||
return ENUM_UNNAMED;
|
||||
auto it = m_enum_index.find(name);
|
||||
return it == m_enum_index.end() ? ENUM_UNNAMED : it->second;
|
||||
}
|
||||
|
||||
void CacheDictionary::save(cereal::BinaryOutputArchive& ar) const
|
||||
{
|
||||
// Checked here rather than left to the caller: an index that wrapped would
|
||||
// be written silently, and nothing downstream could tell.
|
||||
if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES)
|
||||
throw std::runtime_error("preset cache: the option dictionary outgrew the uint16 it is indexed with");
|
||||
ar(m_keys, m_types, m_enum_values);
|
||||
}
|
||||
|
||||
void CacheDictionary::load(cereal::BinaryInputArchive& ar)
|
||||
{
|
||||
ar(m_keys, m_types, m_enum_values);
|
||||
if (m_keys.size() != m_types.size())
|
||||
throw std::runtime_error("preset cache: dictionary key and type tables differ in length");
|
||||
if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES)
|
||||
throw std::runtime_error("preset cache: dictionary is larger than the uint16 it is indexed with");
|
||||
if (m_enum_values.empty() || ! m_enum_values.front().empty())
|
||||
throw std::runtime_error("preset cache: dictionary is missing its unnamed-enum slot");
|
||||
// Resolved once per file: every option read after this is a vector index.
|
||||
m_defs.resize(m_keys.size());
|
||||
for (size_t i = 0; i < m_keys.size(); ++ i) {
|
||||
const ConfigOptionDef* def = print_config_def.get(m_keys[i]);
|
||||
m_defs[i] = (def != nullptr && uint16_t(def->type) == m_types[i]) ? def : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- one config -----------------------------------------------------------
|
||||
|
||||
static void save_enum_option(cereal::BinaryOutputArchive& ar, const ConfigOptionDef& def,
|
||||
const ConfigOption* opt, const CacheDictionary& dict)
|
||||
{
|
||||
const std::vector<int> values = enum_ints(def, opt);
|
||||
ar(uint32_t(values.size()));
|
||||
for (int value : values) {
|
||||
const uint16_t idx = dict.enum_index(enum_name_of(def, value));
|
||||
ar(idx);
|
||||
if (idx == CacheDictionary::ENUM_UNNAMED)
|
||||
ar(int32_t(value));
|
||||
}
|
||||
}
|
||||
|
||||
// `config` may be null, in which case the option is read and dropped.
|
||||
static void load_enum_option(cereal::BinaryInputArchive& ar, ConfigOptionType type,
|
||||
const ConfigOptionDef* def, DynamicPrintConfig* config,
|
||||
const CacheDictionary& dict)
|
||||
{
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
if (type == coEnum && cnt != 1)
|
||||
throw std::runtime_error("preset cache: a scalar enum carrying more than one value");
|
||||
// Every element is read whatever happens, so the stream stays in sync and
|
||||
// whatever follows this option still loads.
|
||||
bool usable = def != nullptr && config != nullptr;
|
||||
std::vector<int> values;
|
||||
values.reserve(cnt);
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
uint16_t idx = 0;
|
||||
ar(idx);
|
||||
if (! dict.valid_enum_index(idx))
|
||||
throw std::runtime_error("preset cache: enum value index past the end of the dictionary");
|
||||
if (idx == CacheDictionary::ENUM_UNNAMED) {
|
||||
// An int the writer could not name — a nil, or an option whose
|
||||
// definition carried no enum_keys_map. It travels verbatim.
|
||||
int32_t raw = 0;
|
||||
ar(raw);
|
||||
values.push_back(int(raw));
|
||||
continue;
|
||||
}
|
||||
if (! usable)
|
||||
continue; // the index above was this element's whole payload
|
||||
if (def->enum_keys_map == nullptr) {
|
||||
usable = false; // this build no longer maps this option's names
|
||||
continue;
|
||||
}
|
||||
const auto it = def->enum_keys_map->find(dict.enum_name_at(idx));
|
||||
if (it == def->enum_keys_map->end()) {
|
||||
usable = false; // a value this build dropped: the option goes with it
|
||||
continue;
|
||||
}
|
||||
values.push_back(it->second);
|
||||
}
|
||||
if (! usable)
|
||||
return;
|
||||
if (type == coEnum) {
|
||||
config->set_key_value(def->opt_key, new ConfigOptionEnumGeneric(def->enum_keys_map, values.front()));
|
||||
} else {
|
||||
auto* opt = def->nullable ? static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGenericNullable(def->enum_keys_map))
|
||||
: static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGeneric(def->enum_keys_map));
|
||||
opt->values = std::move(values);
|
||||
config->set_key_value(def->opt_key, opt);
|
||||
}
|
||||
}
|
||||
|
||||
void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict)
|
||||
{
|
||||
struct Written { uint16_t idx; const ConfigOptionDef* def; const ConfigOption* opt; };
|
||||
std::vector<Written> written;
|
||||
written.reserve(config.size());
|
||||
for (auto it = config.cbegin(); it != config.cend(); ++ it)
|
||||
if (const ConfigOptionDef* def = print_config_def.get(it->first))
|
||||
written.push_back({ dict.key_index(it->first), def, it->second.get() });
|
||||
|
||||
ar(uint32_t(written.size()));
|
||||
for (const Written& w : written) {
|
||||
ar(w.idx);
|
||||
if (w.def->type == coEnum || w.def->type == coEnums)
|
||||
save_enum_option(ar, *w.def, w.opt, dict);
|
||||
else
|
||||
w.def->save_option_to_archive(ar, w.opt);
|
||||
}
|
||||
}
|
||||
|
||||
// `config` null means: read everything, keep nothing.
|
||||
static void read_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig* config, const CacheDictionary& dict)
|
||||
{
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
if (config != nullptr)
|
||||
config->clear();
|
||||
// Reused across the loop: constructing a ConfigOptionDef per dropped option
|
||||
// would allocate its strings and vectors for nothing.
|
||||
ConfigOptionDef scratch;
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
uint16_t idx = 0;
|
||||
ar(idx);
|
||||
if (! dict.valid_key_index(idx))
|
||||
throw std::runtime_error("preset cache: option index past the end of the dictionary");
|
||||
const ConfigOptionType type = dict.type_at(idx);
|
||||
const ConfigOptionDef* def = dict.def_at(idx);
|
||||
if (type == coEnum || type == coEnums) {
|
||||
load_enum_option(ar, type, def, config, dict);
|
||||
} else if (def != nullptr && config != nullptr) {
|
||||
config->set_key_value(def->opt_key, def->load_option_from_archive(ar));
|
||||
} else {
|
||||
// Read by the type the writer recorded, then drop: the same outcome
|
||||
// a JSON profile gets for an option this build no longer has.
|
||||
scratch.type = type;
|
||||
std::unique_ptr<ConfigOption> discard(scratch.load_option_from_archive(ar));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict)
|
||||
{
|
||||
read_config(ar, &config, dict);
|
||||
}
|
||||
|
||||
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict)
|
||||
{
|
||||
read_config(ar, nullptr, dict);
|
||||
}
|
||||
|
||||
// ---- The per-vendor cache file (<vendor>.opc) -----------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct CacheFileHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint64_t data_size;
|
||||
uint32_t crc32;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
|
||||
|
||||
constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
|
||||
// Bump when the wire format changes in a way the payload cannot describe
|
||||
// itself out of: reordering, removing or retyping a field of a hand-written
|
||||
// serialize() (VendorProfile and its nested types, CachedPreset via
|
||||
// save_entries below), or a change to the cache's own layout or the
|
||||
// meaning of its stamps. Option-schema drift is NOT such a change — the
|
||||
// dictionary handles it, which is why this no longer moves every release.
|
||||
constexpr uint32_t CACHE_VERSION = 1;
|
||||
|
||||
// A stamp-string read that refuses an absurd length before allocating anything.
|
||||
// The stamps are read from files named from the outside (peek_version is
|
||||
// pointed at whatever <vendor>.opc a directory holds), so the length word may
|
||||
// be arbitrary bytes — and a resize to a garbage 64-bit length does not fail as
|
||||
// a catchable bad_alloc here, it takes the app down through the out-of-memory
|
||||
// handler. A vendor name or profile version is a short token; anything longer
|
||||
// is not a cache this build wrote.
|
||||
std::string read_bounded_string(cereal::BinaryInputArchive& ar)
|
||||
{
|
||||
constexpr uint64_t MAX_STAMP_LEN = 1024;
|
||||
cereal::size_type len = 0;
|
||||
ar(cereal::make_size_tag(len));
|
||||
if (uint64_t(len) > MAX_STAMP_LEN)
|
||||
throw std::runtime_error("preset cache: string length out of bounds");
|
||||
std::string s(size_t(len), '\0');
|
||||
ar(cereal::binary_data(s.data(), size_t(len)));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The prologue every cache reader starts with: the format version, then the
|
||||
// vendor's identity. Returns the vendor version stamped on a body this build can
|
||||
// read, empty on anything else — which is the same answer as "not this vendor".
|
||||
std::string read_cache_stamps(cereal::BinaryInputArchive& ar, const std::string& expected_vendor_name)
|
||||
{
|
||||
// The version is judged before anything variable-length is read: on a body
|
||||
// that is not a per-vendor cache of this version, the bytes where a string
|
||||
// length would sit may be arbitrary framing.
|
||||
uint32_t cache_version = 0;
|
||||
ar(cache_version);
|
||||
if (cache_version != CACHE_VERSION)
|
||||
return {};
|
||||
const std::string vendor_name = read_bounded_string(ar);
|
||||
const std::string vendor_version = read_bounded_string(ar);
|
||||
if (vendor_name != expected_vendor_name)
|
||||
return {};
|
||||
return vendor_version;
|
||||
}
|
||||
|
||||
// A cache stays usable as long as it was built from a vendor profile at least
|
||||
// as new as the one now on disk. Profiles whose version is invalid cannot be
|
||||
// judged this way and are never served from cache; where no profile sits
|
||||
// beside the cache at all, nothing can be newer than it — that state is passed
|
||||
// as Semver::inf(), which no real profile can carry (an invalid version could
|
||||
// not say it apart from "profile there but unjudgeable", and zero would
|
||||
// collide with a genuine "0.0.0"). This is the serve rule; the install rule
|
||||
// (cache_covers in PresetBundle.cpp) deliberately reads an unjudgeable profile
|
||||
// the other way, so the two are not one function.
|
||||
bool cache_covers_version(const std::string& cached, const Semver& on_disk)
|
||||
{
|
||||
if (on_disk == Semver::inf())
|
||||
return true; // before parsing `cached`: nothing exists that the stamp must cover
|
||||
if (! on_disk.valid())
|
||||
return false;
|
||||
const auto cached_ver = Semver::parse(cached);
|
||||
return cached_ver && *cached_ver >= on_disk;
|
||||
}
|
||||
|
||||
// CachedPreset on the wire: all fields, declaration order, in one place.
|
||||
// `config` writes, reads or skips the config sitting in the middle of that
|
||||
// order — the three things a reader can want to do with it — so save, load and
|
||||
// the name peek below cannot drift apart. Keep in sync with the struct in
|
||||
// PresetCacheFormat.hpp and bump CACHE_VERSION on change. Written here rather
|
||||
// than as a serialize() member because the config needs the file's dictionary,
|
||||
// which cereal cannot thread through one.
|
||||
template<class Archive, class Entry, class ConfigFn>
|
||||
void visit_entry(Archive& ar, Entry& e, ConfigFn&& config)
|
||||
{
|
||||
ar(e.name, e.sub_path);
|
||||
config();
|
||||
ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
|
||||
}
|
||||
|
||||
// The count comes from a file that has already passed magic and CRC, but a
|
||||
// reserve is a promise to allocate: cap it and let push_back grow the rest.
|
||||
constexpr uint32_t MAX_RESERVED_ENTRIES = 4096;
|
||||
|
||||
void save_entries(cereal::BinaryOutputArchive& ar,
|
||||
const std::vector<CachedPreset>& entries,
|
||||
const CacheDictionary& dict)
|
||||
{
|
||||
ar(uint32_t(entries.size()));
|
||||
for (const CachedPreset& e : entries)
|
||||
visit_entry(ar, e, [&] { save_config(ar, e.config_src, dict); });
|
||||
}
|
||||
|
||||
void load_entries(cereal::BinaryInputArchive& ar,
|
||||
std::vector<CachedPreset>& entries,
|
||||
const CacheDictionary& dict)
|
||||
{
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
entries.clear();
|
||||
entries.reserve(std::min(cnt, MAX_RESERVED_ENTRIES));
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
CachedPreset e;
|
||||
visit_entry(ar, e, [&] { load_config(ar, e.config_src, dict); });
|
||||
entries.push_back(std::move(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Read a raw cache body: verify magic, size, CRC.
|
||||
bool read_cache_blob(const std::string& path, std::string& out_blob)
|
||||
{
|
||||
try {
|
||||
boost::nowide::ifstream ifs(path, std::ios::binary);
|
||||
if (!ifs.is_open())
|
||||
return false;
|
||||
CacheFileHeader fhdr;
|
||||
if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)))
|
||||
return false;
|
||||
if (fhdr.magic != CACHE_MAGIC)
|
||||
return false;
|
||||
// data_size is 8 bytes from a file nothing has authenticated yet, and
|
||||
// it is about to size an allocation. The body is the whole of the file
|
||||
// behind the header — anything else is not a cache this build wrote.
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const std::streamoff file_size = ifs.tellg();
|
||||
if (file_size < std::streamoff(sizeof(fhdr)) ||
|
||||
fhdr.data_size == 0 ||
|
||||
fhdr.data_size != uint64_t(file_size) - sizeof(fhdr))
|
||||
return false;
|
||||
ifs.seekg(sizeof(fhdr), std::ios::beg);
|
||||
out_blob.assign(fhdr.data_size, '\0');
|
||||
if (!ifs.read(&out_blob[0], static_cast<std::streamsize>(fhdr.data_size)))
|
||||
return false;
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(out_blob.data(), out_blob.size());
|
||||
if (crc.checksum() != fhdr.crc32) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: CRC mismatch: " << path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: read failed (" << path << "): " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Write a cache body behind the standard 20-byte file header. False when the
|
||||
// file could not be opened or written whole.
|
||||
bool write_cache_blob(const std::string& path, const std::string& blob)
|
||||
{
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(blob.data(), blob.size());
|
||||
// Written beside the target and moved into place, as AppConfig::save does:
|
||||
// a cache is truncated and rewritten in full, so a write that dies partway
|
||||
// would otherwise leave a header claiming more body than the file holds.
|
||||
// The PID suffix also keeps two instances writing the same vendor from
|
||||
// interleaving.
|
||||
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
|
||||
try {
|
||||
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
|
||||
return false;
|
||||
}
|
||||
CacheFileHeader fhdr;
|
||||
fhdr.magic = CACHE_MAGIC;
|
||||
fhdr.version = CACHE_VERSION;
|
||||
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
||||
fhdr.crc32 = crc.checksum();
|
||||
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
||||
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
|
||||
ofs.close(); // flush; close() raises failbit on error
|
||||
if (! ofs.good()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (const std::error_code ec = rename_file(tmp_path, path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
|
||||
boost::system::error_code rm;
|
||||
boost::filesystem::remove(tmp_path, rm);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// static
|
||||
bool VendorCacheFile::save(const std::string& path, const std::string& vendor_name,
|
||||
const std::string& vendor_version, const VendorCacheData& data)
|
||||
{
|
||||
try {
|
||||
// Collected before anything is written: the dictionary sits ahead of the
|
||||
// entries so a reader resolves it once and then indexes.
|
||||
CacheDictionary dict;
|
||||
for (const std::vector<CachedPreset>* entries : { &data.process_entries, &data.filament_entries, &data.machine_entries })
|
||||
for (const CachedPreset& e : *entries)
|
||||
dict.collect(e.config_src);
|
||||
|
||||
std::ostringstream body(std::ios::binary);
|
||||
{
|
||||
cereal::BinaryOutputArchive ar(body);
|
||||
ar(CACHE_VERSION);
|
||||
ar(vendor_name, vendor_version);
|
||||
dict.save(ar);
|
||||
ar(data.vendors);
|
||||
save_entries(ar, data.process_entries, dict);
|
||||
save_entries(ar, data.filament_entries, dict);
|
||||
save_entries(ar, data.machine_entries, dict);
|
||||
ar(data.parse_errors);
|
||||
}
|
||||
return write_cache_blob(path, body.str());
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: failed to save vendor cache " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name,
|
||||
const Semver& expected_vendor_version, VendorCacheData& data)
|
||||
{
|
||||
std::string blob;
|
||||
if (! read_cache_blob(path, blob))
|
||||
return false;
|
||||
try {
|
||||
// Read in place: an istringstream would copy the blob once more just to
|
||||
// stream over it.
|
||||
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
const std::string vendor_version = read_cache_stamps(ar, expected_vendor_name);
|
||||
if (vendor_version.empty() || ! cache_covers_version(vendor_version, expected_vendor_version))
|
||||
return false;
|
||||
CacheDictionary dict;
|
||||
dict.load(ar);
|
||||
ar(data.vendors);
|
||||
load_entries(ar, data.process_entries, dict);
|
||||
load_entries(ar, data.filament_entries, dict);
|
||||
load_entries(ar, data.machine_entries, dict);
|
||||
ar(data.parse_errors);
|
||||
if (data.vendors.find(expected_vendor_name) == data.vendors.end())
|
||||
throw std::runtime_error("vendor cache does not carry its own vendor profile");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: rejecting vendor cache " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name)
|
||||
{
|
||||
try {
|
||||
boost::nowide::ifstream ifs(path, std::ios::binary);
|
||||
CacheFileHeader fhdr;
|
||||
if (! ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)) || fhdr.magic != CACHE_MAGIC)
|
||||
return {};
|
||||
// Only the head of the body is read, and its CRC left unverified: the
|
||||
// stamps sit at the front, and this answers "what version is this?"
|
||||
// without paying for tens of megabytes. Callers that need to know the
|
||||
// file is whole use usable_version instead.
|
||||
std::string head(static_cast<size_t>(std::min<uint64_t>(fhdr.data_size, 1024)), '\0');
|
||||
if (! ifs.read(&head[0], static_cast<std::streamsize>(head.size())))
|
||||
return {};
|
||||
std::istringstream body(head, std::ios::binary);
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
return read_cache_stamps(ar, expected_vendor_name);
|
||||
} catch (const std::exception&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
Semver VendorCacheFile::usable_version(const std::string& path, const std::string& expected_vendor_name)
|
||||
{
|
||||
std::string blob;
|
||||
if (! read_cache_blob(path, blob))
|
||||
return Semver::invalid();
|
||||
try {
|
||||
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
const auto ver = Semver::parse(read_cache_stamps(ar, expected_vendor_name));
|
||||
return ver ? *ver : Semver::invalid();
|
||||
} catch (const std::exception&) {
|
||||
return Semver::invalid();
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool VendorCacheFile::carries_preset(const std::string& path, const std::string& vendor_name,
|
||||
Preset::Type type, const std::string& preset_name)
|
||||
{
|
||||
std::string blob;
|
||||
if (! read_cache_blob(path, blob))
|
||||
return false;
|
||||
try {
|
||||
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
if (read_cache_stamps(ar, vendor_name).empty())
|
||||
return false;
|
||||
CacheDictionary dict;
|
||||
dict.load(ar);
|
||||
VendorMap vendors;
|
||||
ar(vendors);
|
||||
// Reused: every entry overwrites it, and only its name is ever looked at.
|
||||
CachedPreset entry;
|
||||
// Written in this order by save. The list that could carry the preset
|
||||
// is the last one worth reading.
|
||||
for (Preset::Type kind : { Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER }) {
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
visit_entry(ar, entry, [&] { skip_config(ar, dict); });
|
||||
if (kind == type && entry.name == preset_name)
|
||||
return true;
|
||||
}
|
||||
if (kind == type)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not read preset names from " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,192 @@
|
||||
#ifndef slic3r_PresetCacheFormat_hpp_
|
||||
#define slic3r_PresetCacheFormat_hpp_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <cereal/archives/binary.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// How the preset cache writes a DynamicPrintConfig.
|
||||
//
|
||||
// Not through the global cereal hooks in PrintConfig.hpp: those key an option by
|
||||
// its serialization_key_ordinal, which ConfigDef::add assigns by declaration
|
||||
// order at static-init time. Inserting one option into the middle of
|
||||
// PrintConfig.cpp shifts every later ordinal, and the lookup on the way back in
|
||||
// then SUCCEEDS on the wrong option — where the two share a type, and hundreds
|
||||
// of coFloat/coBool/coInt options do, the bytes deserialize cleanly into the
|
||||
// wrong key. Silently wrong print settings, no error. Those hooks are also the
|
||||
// undo/redo wire format, where the process cannot change underneath them, so
|
||||
// they stay as they are and the cache keys by name instead.
|
||||
//
|
||||
// Names are not repeated per preset. Each cache file carries one dictionary of
|
||||
// the distinct opt_keys it uses, the type each was written as, and the distinct
|
||||
// enum value names; an option on the wire is then a uint16 index into it plus
|
||||
// its value. The dictionary is resolved to this build's option definitions once
|
||||
// per file, after which reading an option is a vector index.
|
||||
class CacheDictionary
|
||||
{
|
||||
public:
|
||||
CacheDictionary();
|
||||
|
||||
// Index reserved in the enum table for an int the writing build could not
|
||||
// name — a nullable option's nil, or a definition carrying no
|
||||
// enum_keys_map. The raw int32 follows it on the wire and is loaded
|
||||
// verbatim, so those values survive too.
|
||||
static constexpr uint16_t ENUM_UNNAMED = 0;
|
||||
|
||||
// ---- writing ----
|
||||
|
||||
// Record every key and enum value `config` uses. Call for every config that
|
||||
// will be written, before writing the dictionary.
|
||||
void collect(const DynamicPrintConfig& config);
|
||||
|
||||
uint16_t key_index(const t_config_option_key& key) const;
|
||||
// ENUM_UNNAMED for an empty name or one that was never collected.
|
||||
uint16_t enum_index(const std::string& name) const;
|
||||
|
||||
// ---- reading ----
|
||||
|
||||
// The definition an index resolves to in THIS build, or nullptr where the
|
||||
// key is unknown here or is now defined with a different type. A nullptr
|
||||
// entry's value is still read — using type_at(idx), the type the writer
|
||||
// recorded — and then dropped, which is what a JSON profile gets for an
|
||||
// option this build no longer has.
|
||||
const ConfigOptionDef* def_at(uint16_t idx) const { return m_defs[idx]; }
|
||||
ConfigOptionType type_at(uint16_t idx) const { return ConfigOptionType(m_types[idx]); }
|
||||
const std::string& enum_name_at(uint16_t idx) const { return m_enum_values[idx]; }
|
||||
// m_defs, not m_keys: only load() sizes it, so this is false for every index
|
||||
// on a dictionary that was collected rather than read.
|
||||
bool valid_key_index(uint16_t idx) const { return size_t(idx) < m_defs.size(); }
|
||||
bool valid_enum_index(uint16_t idx) const { return size_t(idx) < m_enum_values.size(); }
|
||||
|
||||
// The layout these two agree on is covered by CACHE_VERSION (PresetCacheFormat.cpp);
|
||||
// bump it when they change.
|
||||
// Throws when either table outgrew the uint16 the wire format indexes it
|
||||
// with. Both are bounded by the option count (912 at the time of writing), so
|
||||
// that is a build-time failure in CI, not a runtime one.
|
||||
void save(cereal::BinaryOutputArchive& ar) const;
|
||||
// Throws on a dictionary that cannot be indexed as written.
|
||||
void load(cereal::BinaryInputArchive& ar);
|
||||
|
||||
private:
|
||||
// Indices are uint16, so a table may hold at most this many entries.
|
||||
static constexpr size_t MAX_ENTRIES = 0xFFFF;
|
||||
|
||||
std::vector<std::string> m_keys;
|
||||
// ConfigOptionType, as written. Sixteen bits, not eight: coVectorType is
|
||||
// 0x4000, so every vector type — coFloats, coEnums, coStrings — is above
|
||||
// 255, and a byte would fold each one onto its scalar counterpart.
|
||||
std::vector<uint16_t> m_types;
|
||||
std::vector<std::string> m_enum_values; // [ENUM_UNNAMED] is always empty
|
||||
|
||||
// Writing.
|
||||
std::unordered_map<std::string, uint16_t> m_key_index;
|
||||
std::unordered_map<std::string, uint16_t> m_enum_index;
|
||||
// Reading, resolved once by load().
|
||||
std::vector<const ConfigOptionDef*> m_defs;
|
||||
};
|
||||
|
||||
// One config, keyed through `dict`. Options print_config_def does not know are
|
||||
// not written: nothing could give them a type on the way back in.
|
||||
void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict);
|
||||
// Throws only on a payload that cannot be indexed; an option this build cannot
|
||||
// place is dropped, not fatal.
|
||||
void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict);
|
||||
// Consume one config without building it, for a reader that only wants what
|
||||
// comes after.
|
||||
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
|
||||
|
||||
// One preset as its JSON subfile states it: the config diff, the name of the
|
||||
// preset it inherits, and the parse metadata — everything the parse phase of
|
||||
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
|
||||
// is resolved when the entry is installed, against whatever filament library
|
||||
// is loaded then, so a cache carries no other vendor's values and no other
|
||||
// vendor's update can make it stale.
|
||||
// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every
|
||||
// field below in this order — once, for the save, the load and the name peek alike.
|
||||
struct CachedPreset
|
||||
{
|
||||
std::string name;
|
||||
std::string sub_path; // path under the vendor's directory
|
||||
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
|
||||
std::string inherits;
|
||||
std::string description;
|
||||
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
|
||||
std::string setting_id;
|
||||
std::string filament_id;
|
||||
std::vector<std::string> renamed_from;
|
||||
};
|
||||
|
||||
// What one per-vendor cache file carries besides its stamps: the vendor profile
|
||||
// map, the presets in source form, and how many errors their parse counted.
|
||||
struct VendorCacheData
|
||||
{
|
||||
VendorMap vendors;
|
||||
std::vector<CachedPreset> process_entries;
|
||||
std::vector<CachedPreset> filament_entries;
|
||||
std::vector<CachedPreset> machine_entries;
|
||||
uint64_t parse_errors = 0;
|
||||
};
|
||||
|
||||
// A per-vendor preset cache file (<vendor>.opc): a 20-byte header (magic, format
|
||||
// version, body size, CRC) framing one cereal body — stamps (format version,
|
||||
// vendor name, vendor profile version), the option dictionary, then the
|
||||
// VendorCacheData. Everything about those bytes lives here; when a vendor is
|
||||
// served from its cache, and how entries install into a bundle, is
|
||||
// PresetBundle's business.
|
||||
class VendorCacheFile
|
||||
{
|
||||
public:
|
||||
// Save one vendor (vendor_name at vendor_version). False when the file
|
||||
// could not be written whole.
|
||||
static bool save(const std::string& path, const std::string& vendor_name,
|
||||
const std::string& vendor_version, const VendorCacheData& data);
|
||||
|
||||
// Read a whole cache into `data`. False — with `data` in an unspecified
|
||||
// state — unless the file is a cache this build wrote, its CRC holds, it
|
||||
// names this vendor, it was built from a vendor profile at least as new as
|
||||
// `expected_vendor_version`, and it carries its own vendor profile. An
|
||||
// invalid expected version (a profile whose version
|
||||
// cannot be judged) is never served from cache; Semver::inf() (no profile
|
||||
// beside the cache at all) accepts whatever is cached.
|
||||
static bool load(const std::string& path, const std::string& expected_vendor_name,
|
||||
const Semver& expected_vendor_version, VendorCacheData& data);
|
||||
|
||||
// Read the profile version a cache was stamped with, without deserializing
|
||||
// its presets. Empty if the file is unreadable, not a cache this build
|
||||
// understands, or not this vendor's. This is how an installed vendor's
|
||||
// version is known when only its cache is installed.
|
||||
static std::string peek_version(const std::string& path, const std::string& expected_vendor_name);
|
||||
|
||||
// The profile version an installed cache can actually be served at, or an
|
||||
// invalid Semver when the file is not a cache this build can read. Unlike
|
||||
// peek_version this verifies the body's CRC, at the cost of reading the
|
||||
// whole file: where the cache is the vendor's whole installation, "a file
|
||||
// is there" is not enough to call it installed, and a vendor wrongly
|
||||
// believed installed is never repaired.
|
||||
static Semver usable_version(const std::string& path, const std::string& expected_vendor_name);
|
||||
|
||||
// Whether a cache carries a preset of `type` under `preset_name`, without
|
||||
// installing any of them. False when the file is not a cache this build can
|
||||
// read. The three kinds are written in one stream, so reaching the machines
|
||||
// means reading past the processes and filaments — their configs are consumed
|
||||
// and dropped rather than built. This is how a build that ships caches instead
|
||||
// of preset JSONs answers "which vendor carries this preset?".
|
||||
static bool carries_preset(const std::string& path, const std::string& vendor_name,
|
||||
Preset::Type type, const std::string& preset_name);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PresetCacheFormat_hpp_
|
||||
+117
-33
@@ -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();
|
||||
@@ -3712,7 +3790,7 @@ std::vector<Polygons> Print::get_extruder_printable_polygons() const
|
||||
Polygons ploys = {Polygon::new_scale(e_printable_area)};
|
||||
extruder_printable_polys.emplace_back(ploys);
|
||||
}
|
||||
return std::move(extruder_printable_polys);
|
||||
return extruder_printable_polys;
|
||||
}
|
||||
|
||||
std::vector<Polygons> Print::get_extruder_unprintable_polygons() const
|
||||
@@ -3725,7 +3803,7 @@ std::vector<Polygons> Print::get_extruder_unprintable_polygons() const
|
||||
Polygons ploys = diff(printable_poly, Polygon::new_scale(e_printable_area));
|
||||
extruder_unprintable_polys.emplace_back(ploys);
|
||||
}
|
||||
return std::move(extruder_unprintable_polys);
|
||||
return extruder_unprintable_polys;
|
||||
}
|
||||
|
||||
size_t Print::get_extruder_id(unsigned int filament_id) const
|
||||
@@ -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();
|
||||
@@ -5827,7 +5911,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
|
||||
|
||||
Polygon PrintInstance::get_convex_hull_2d() {
|
||||
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
|
||||
poly.douglas_peucker(0.1);
|
||||
poly.douglas_peucker(scale_(0.1));
|
||||
return poly;
|
||||
}
|
||||
|
||||
|
||||
+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};
|
||||
|
||||
+127
-15
@@ -1,6 +1,7 @@
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cfloat>
|
||||
@@ -559,11 +560,9 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv)
|
||||
|
||||
static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo)
|
||||
{
|
||||
// Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement.
|
||||
Transform3d object_trafo_local = object_trafo;
|
||||
object_trafo_local.translation().x() = 0.;
|
||||
object_trafo_local.translation().y() = 0.;
|
||||
Transform3d m = object_trafo_local * volume_trafo;
|
||||
Transform3d m = object_trafo * volume_trafo;
|
||||
m.translation().x() = 0.;
|
||||
m.translation().y() = 0.;
|
||||
return m.cast<float>();
|
||||
}
|
||||
|
||||
@@ -888,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;
|
||||
}
|
||||
@@ -980,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>();
|
||||
@@ -1015,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];
|
||||
@@ -1036,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()) {
|
||||
@@ -1123,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();
|
||||
}
|
||||
|
||||
@@ -1143,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());
|
||||
@@ -1804,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.
|
||||
@@ -1830,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];
|
||||
}
|
||||
@@ -1864,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.
|
||||
@@ -1886,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
|
||||
@@ -2488,7 +2497,8 @@ namespace cereal {
|
||||
archive(serialization_key_ordinal);
|
||||
assert(serialization_key_ordinal > 0);
|
||||
auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal);
|
||||
assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end());
|
||||
if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end())
|
||||
throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale");
|
||||
config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,7 +906,7 @@ void PrintObject::detect_overhangs_for_lift()
|
||||
Layer& lower_layer = *layer.lower_layer;
|
||||
|
||||
ExPolygons overhangs = diff_ex(layer.lslices, offset_ex(lower_layer.lslices, scale_(min_overlap)));
|
||||
layer.loverhangs = std::move(offset2_ex(overhangs, -0.1f * scale_(line_width), 0.1f * scale_(line_width)));
|
||||
layer.loverhangs = offset2_ex(overhangs, -0.1f * scale_(line_width), 0.1f * scale_(line_width));
|
||||
layer.loverhangs_bbox = get_extents(layer.loverhangs);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
|
||||
#include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
|
||||
|
||||
@@ -190,6 +190,19 @@ public:
|
||||
os << self.to_string();
|
||||
return os;
|
||||
}
|
||||
|
||||
// cereal: round-trip through the standard 3-part string (major.minor.patch).
|
||||
// to_string() uses a BBS 4-part format that semver_parse() cannot read back.
|
||||
template<class Archive>
|
||||
std::string save_minimal(const Archive&) const { return to_string_sf(); }
|
||||
template<class Archive>
|
||||
void load_minimal(const Archive&, const std::string& s) {
|
||||
auto v = Semver::parse(s);
|
||||
if (! v)
|
||||
throw std::runtime_error("Semver: cannot parse serialized version: " + s);
|
||||
*this = std::move(*v);
|
||||
}
|
||||
|
||||
private:
|
||||
semver_t ver;
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ static void MakeMesh(TopoDS_Shape& theSolid, TriangleMesh& theMesh)
|
||||
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
|
||||
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
|
||||
aPnt.Transform(aTrsf);
|
||||
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
|
||||
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
|
||||
}
|
||||
//BBS: copy triangles
|
||||
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
|
||||
|
||||
@@ -842,7 +842,7 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
|
||||
// normal overhang
|
||||
ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS);
|
||||
overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted));
|
||||
overhangs_all_layers[layer_nr] = diff_ex(curr_polys, lower_layer_offseted);
|
||||
|
||||
double duration{ std::chrono::duration_cast<second_>(clock_::now() - t0).count() };
|
||||
if (duration > 30 || overhangs_all_layers[layer_nr].size() > 100) {
|
||||
@@ -1396,7 +1396,7 @@ void TreeSupport::generate_toolpaths()
|
||||
raft_areas.push_back(expoly);
|
||||
}
|
||||
|
||||
raft_areas = std::move(offset_ex(raft_areas, scale_(object_config.raft_first_layer_expansion)));
|
||||
raft_areas = offset_ex(raft_areas, scale_(object_config.raft_first_layer_expansion));
|
||||
|
||||
size_t layer_nr = 0;
|
||||
for (; layer_nr < m_slicing_params.base_raft_layers; layer_nr++) {
|
||||
@@ -1522,9 +1522,9 @@ void TreeSupport::generate_toolpaths()
|
||||
erSupportMaterialInterface : erSupportMaterial;
|
||||
make_perimeter_and_inner_brim(ts_layer->support_fills.entities, poly, wall_count, flow,
|
||||
brim_role);
|
||||
polys = std::move(offset_ex(poly, -flow.scaled_spacing()));
|
||||
polys = offset_ex(poly, -flow.scaled_spacing());
|
||||
} else if (area_group.type == SupportLayer::Roof1stLayer) {
|
||||
polys = std::move(offset_ex(poly, 0.5*support_flow.scaled_width()));
|
||||
polys = offset_ex(poly, 0.5*support_flow.scaled_width());
|
||||
}
|
||||
else {
|
||||
polys.push_back(poly);
|
||||
@@ -2269,7 +2269,7 @@ void TreeSupport::draw_circles()
|
||||
// Inside the gap: remove only the part overlapping the contact surface, keep the rest.
|
||||
if (bottom_gap_height > EPSILON && layer_bottom_z < band_gap_top - EPSILON) {
|
||||
any_gap_cleared = true;
|
||||
comp_poly = std::move(diff_ex(comp_poly, band.surfaces));
|
||||
comp_poly = diff_ex(comp_poly, band.surfaces);
|
||||
}
|
||||
|
||||
// Overlaps interface band
|
||||
@@ -2304,7 +2304,7 @@ void TreeSupport::draw_circles()
|
||||
ExPolygons comp_interface = band_ex.empty() ? ExPolygons {} : intersection_ex(comp_poly, band_ex);
|
||||
if (!comp_interface.empty()) {
|
||||
append(new_floor_areas, comp_interface);
|
||||
comp_poly = std::move(diff_ex(comp_poly, offset_ex(comp_interface, 10)));
|
||||
comp_poly = diff_ex(comp_poly, offset_ex(comp_interface, 10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2396,7 +2396,7 @@ void TreeSupport::draw_circles()
|
||||
ts_layer->lslices.emplace_back(*expoly);
|
||||
}
|
||||
|
||||
ts_layer->lslices = std::move(union_ex(ts_layer->lslices));
|
||||
ts_layer->lslices = union_ex(ts_layer->lslices);
|
||||
//Must update bounding box which is used in avoid crossing perimeter
|
||||
ts_layer->lslices_bboxes.clear();
|
||||
ts_layer->lslices_bboxes.reserve(ts_layer->lslices.size());
|
||||
@@ -2474,7 +2474,7 @@ void TreeSupport::draw_circles()
|
||||
if (global_lightning_infill)
|
||||
{
|
||||
//search overhangs globally
|
||||
overhang = std::move(diff_ex(offset_ex(base_areas_lower, -2.0 * scale_(support_extrusion_width)), base_areas));
|
||||
overhang = diff_ex(offset_ex(base_areas_lower, -2.0 * scale_(support_extrusion_width)), base_areas);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2485,13 +2485,13 @@ void TreeSupport::draw_circles()
|
||||
Polygon rev_hole = hole;
|
||||
rev_hole.make_counter_clockwise();
|
||||
ExPolygons ex_hole;
|
||||
ex_hole.emplace_back(std::move(ExPolygon(rev_hole)));
|
||||
ex_hole.emplace_back(ExPolygon(rev_hole));
|
||||
for (auto& other_area : base_areas)
|
||||
//if (&other_area != &base_area)
|
||||
ex_hole = std::move(diff_ex(ex_hole, other_area));
|
||||
overhang = std::move(union_ex(overhang, ex_hole));
|
||||
ex_hole = diff_ex(ex_hole, other_area);
|
||||
overhang = union_ex(overhang, ex_hole);
|
||||
}
|
||||
overhang = std::move(intersection_ex(overhang, offset_ex(base_areas_lower, -0.5 * scale_(support_extrusion_width))));
|
||||
overhang = intersection_ex(overhang, offset_ex(base_areas_lower, -0.5 * scale_(support_extrusion_width)));
|
||||
}
|
||||
|
||||
overhangs.emplace_back(to_polygons(overhang));
|
||||
@@ -2746,7 +2746,7 @@ void TreeSupport::drop_nodes()
|
||||
|
||||
m_object->print()->set_status(60 + int(10 * (1 - float(layer_nr) / contact_nodes.size())), _u8L("Generating support"));// (boost::format(_u8L("Support: propagate branches at layer %d")) % layer_nr).str());
|
||||
|
||||
Polygons layer_contours = std::move(m_ts_data->get_contours_with_holes(obj_layer_nr));
|
||||
Polygons layer_contours = m_ts_data->get_contours_with_holes(obj_layer_nr);
|
||||
//std::unordered_map<Line, bool, LineHash>& mst_line_x_layer_contour_cache = m_mst_line_x_layer_contour_caches[layer_nr];
|
||||
tbb::concurrent_unordered_map<Line, bool, LineHash> mst_line_x_layer_contour_cache;
|
||||
auto is_line_cut_by_contour = [&mst_line_x_layer_contour_cache,&layer_contours](Point a, Point b)
|
||||
@@ -3763,7 +3763,7 @@ const ExPolygons& TreeSupportData::calculate_avoidance(const RadiusLayerPair& ke
|
||||
}
|
||||
const ExPolygons &collision = get_collision(radius, layer_nr);
|
||||
avoidance_areas.insert(avoidance_areas.end(), collision.begin(), collision.end());
|
||||
avoidance_areas = std::move(union_ex(avoidance_areas));
|
||||
avoidance_areas = union_ex(avoidance_areas);
|
||||
auto ret = m_avoidance_cache.insert({key, std::move(avoidance_areas)});
|
||||
//assert(ret.second);
|
||||
return ret.first->second;
|
||||
|
||||
@@ -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,13 +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]);
|
||||
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()) {
|
||||
@@ -1626,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);
|
||||
@@ -1666,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
|
||||
@@ -1871,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.
|
||||
@@ -1878,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)) {
|
||||
@@ -1899,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
|
||||
{
|
||||
@@ -1945,6 +2040,7 @@ Polygons slice_mesh(
|
||||
const MeshSlicingParams ¶ms)
|
||||
{
|
||||
std::vector<IntersectionLines> lines;
|
||||
CoplanarEdges coplanar;
|
||||
|
||||
{
|
||||
bool trafo_identity = is_identity(params.trafo);
|
||||
@@ -1980,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.
|
||||
@@ -1995,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();
|
||||
}
|
||||
|
||||
@@ -1743,13 +1743,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.
|
||||
@@ -1817,6 +1826,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) {
|
||||
@@ -1825,8 +1840,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)
|
||||
@@ -1923,7 +1938,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;
|
||||
@@ -1953,9 +1975,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();
|
||||
@@ -1990,6 +2016,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
|
||||
@@ -376,6 +394,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);
|
||||
|
||||
+38
-5
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <iomanip>
|
||||
#include <locale>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
@@ -18,6 +19,7 @@
|
||||
#include <openssl/md5.h>
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Semver.hpp"
|
||||
|
||||
//define CLI errors
|
||||
|
||||
@@ -722,11 +724,42 @@ void copy_directory_recursively(const boost::filesystem::path& source,
|
||||
std::function<bool(const std::string)> filter = nullptr,
|
||||
bool merge_mode = false);
|
||||
|
||||
// Install vendor bundles from resources directory to data directory
|
||||
// bundle_names: vector of vendor bundle names (without .json extension)
|
||||
// resource_subdir: subdirectory under resources_dir() (default: "profiles")
|
||||
// data_subdir: subdirectory under data_dir() (default: "system")
|
||||
// Returns: true if all bundles installed successfully, false otherwise
|
||||
// ---- Vendor installation on disk ------------------------------------------
|
||||
// How a vendor bundle is installed from resources into data_dir()/system: as
|
||||
// its profile and preset JSONs or, in a build that ships preset caches, as its
|
||||
// .opc preset cache alone. Loading what is installed is PresetBundle's business;
|
||||
// the cache file format itself is VendorCacheFile's (PresetCacheFormat.hpp).
|
||||
|
||||
// True if `vendor` is installed in data_dir()/system. A build that ships preset
|
||||
// caches installs the cache alone, so it — not the profile — marks a vendor
|
||||
// installed; a cache this build cannot read marks nothing.
|
||||
bool is_vendor_installed(const std::string& vendor);
|
||||
|
||||
// The version the installed vendor would be loaded at: its cache's stamp while
|
||||
// that covers the profile beside it, the profile's own version once it does not.
|
||||
// Invalid Semver if neither form is installed.
|
||||
Semver installed_vendor_version(const std::string& vendor);
|
||||
|
||||
// Remove every form `vendor` can be installed as from data_dir()/system: its
|
||||
// profile, its preset cache, and its preset directory.
|
||||
void remove_installed_vendor(const std::string& vendor);
|
||||
|
||||
// The vendors `dir` holds, sorted: one is named by its profile or, in a build that
|
||||
// ships preset caches instead of the raw profile JSONs, by its cache alone.
|
||||
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir);
|
||||
|
||||
// The version a build ships `vendor` at: whichever of its preset cache and its
|
||||
// profile is newer, that being the one installing lays down. Invalid Semver if the
|
||||
// build ships neither.
|
||||
Semver resource_vendor_version(const std::string& vendor);
|
||||
|
||||
// Install vendors from the resources directory into the data directory, each as
|
||||
// its preset cache or as its profile and preset JSONs — whichever of the two the
|
||||
// build ships at the newer version. Anything the previous install of that vendor
|
||||
// left behind goes, so only the form just installed is there to be loaded.
|
||||
// bundle_names: vendor names, without extension.
|
||||
// Every bundle that can be installed is, whatever the others do. Returns false
|
||||
// if any named bundle could not be installed.
|
||||
bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
|
||||
const std::string& resource_subdir = "profiles",
|
||||
const std::string& data_subdir = "system");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+141
-13
@@ -17,6 +17,10 @@
|
||||
#include "Platform.hpp"
|
||||
#include "Time.hpp"
|
||||
#include "libslic3r.h"
|
||||
// For the vendor-installation helpers: the vendor profile version
|
||||
// (get_version_from_json) and the preset cache stamp (VendorCacheFile).
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "MacUtils.hpp"
|
||||
@@ -1724,6 +1728,85 @@ void copy_directory_recursively(const boost::filesystem::path& source,
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Vendor installation on disk ------------------------------------------
|
||||
|
||||
// Whether a cache stamped `cache_ver` still speaks for a vendor whose profile on
|
||||
// disk claims `profile_ver`: it does unless the profile has moved ahead of it. A
|
||||
// profile that is missing or carries no judgeable version cannot be ahead of
|
||||
// anything. The one rule behind both "which form gets installed" and "which form
|
||||
// is installed"; they must not drift apart. Deliberately NOT the serve rule
|
||||
// (VendorCacheFile::load), which refuses an unjudgeable profile instead.
|
||||
static bool cache_covers(const Semver& cache_ver, const Semver& profile_ver)
|
||||
{
|
||||
return cache_ver.valid() && (! profile_ver.valid() || cache_ver >= profile_ver);
|
||||
}
|
||||
|
||||
bool is_vendor_installed(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||
// A cache is the whole of a cache-only installation, so a file this build
|
||||
// cannot serve the vendor from is not an installation. Left counted as one,
|
||||
// the updater would never lay a working copy down.
|
||||
return boost::filesystem::exists(dir / (vendor + ".json"))
|
||||
|| VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor).valid();
|
||||
}
|
||||
|
||||
Semver installed_vendor_version(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||
const boost::filesystem::path json = dir / (vendor + ".json");
|
||||
// Guarded: get_version_from_json logs an error and throws-and-catches its way
|
||||
// to an invalid version on a file that is not there, and a cache-only vendor
|
||||
// never has one.
|
||||
const Semver from_json = boost::filesystem::exists(json) ? get_version_from_json(json.string()) : Semver();
|
||||
const Semver from_cache = VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor);
|
||||
// Whichever form a load would serve.
|
||||
return cache_covers(from_cache, from_json) ? from_cache : from_json;
|
||||
}
|
||||
|
||||
void remove_installed_vendor(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||
boost::filesystem::remove(dir / (vendor + ".json"));
|
||||
boost::filesystem::remove(dir / (vendor + ".opc"));
|
||||
if (boost::filesystem::exists(dir / vendor))
|
||||
boost::filesystem::remove_all(dir / vendor);
|
||||
}
|
||||
|
||||
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir)
|
||||
{
|
||||
std::set<std::string> names;
|
||||
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
|
||||
const auto& path = dir_entry.path();
|
||||
if (Slic3r::is_json_file(path.string()) || path.extension() == ".opc")
|
||||
names.insert(path.stem().string());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// A vendor's preset cache is the whole of its installation: it carries the presets,
|
||||
// the vendor profile and the version they were built at, so where one ships nothing
|
||||
// else needs copying. Unless the profile beside it claims a newer version — a cache
|
||||
// generated before that profile was bumped is out of date, and a cache that cannot
|
||||
// be read is no installation at all — and the vendor is installed the way it was
|
||||
// before caches existed, as its profile and the preset JSONs it points at. Returns
|
||||
// the version the cache is stamped with, invalid when it is not the form to install.
|
||||
static Semver installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor)
|
||||
{
|
||||
const auto cache_ver = Semver::parse(VendorCacheFile::peek_version((dir / (vendor + ".opc")).string(), vendor));
|
||||
if (! cache_ver)
|
||||
return Semver::invalid();
|
||||
const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string());
|
||||
return cache_covers(*cache_ver, profile_ver) ? *cache_ver : Semver::invalid();
|
||||
}
|
||||
|
||||
Semver resource_vendor_version(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles";
|
||||
const Semver ver = installable_cache_version(dir, vendor);
|
||||
return ver.valid() ? ver : get_version_from_json((dir / (vendor + ".json")).string());
|
||||
}
|
||||
|
||||
bool install_vendor_bundles_from_resources(
|
||||
const std::vector<std::string>& bundle_names,
|
||||
const std::string& resource_subdir,
|
||||
@@ -1736,37 +1819,82 @@ bool install_vendor_bundles_from_resources(
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources...";
|
||||
|
||||
// One vendor that cannot be installed is one vendor missing, not a reason to
|
||||
// leave the rest uninstalled. The caller is told, and every bundle that can
|
||||
// be laid down is.
|
||||
bool all_installed = true;
|
||||
|
||||
for (const auto &bundle : bundle_names) {
|
||||
try {
|
||||
if (bundle.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Refusing to install a bundle with no name";
|
||||
all_installed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Install the JSON file
|
||||
auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json");
|
||||
auto path_in_vendors = (vendor_path / bundle).replace_extension(".json");
|
||||
auto cache_in_rsrc = (rsrc_path / bundle).replace_extension(".opc");
|
||||
auto cache_in_vendors = (vendor_path / bundle).replace_extension(".opc");
|
||||
|
||||
if (!fs::exists(path_in_rsrc)) {
|
||||
// Either form of the vendor will do: a build may ship it as a cache alone.
|
||||
if (!fs::exists(path_in_rsrc) && !fs::exists(cache_in_rsrc)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle;
|
||||
return false;
|
||||
all_installed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create target directory if needed
|
||||
if (!fs::exists(vendor_path))
|
||||
fs::create_directories(vendor_path);
|
||||
|
||||
// Copy JSON file
|
||||
std::string error_message;
|
||||
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
|
||||
if (cfr != CopyFileResult::SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
|
||||
return false;
|
||||
bool installed_cache = false;
|
||||
if (installable_cache_version(rsrc_path, bundle).valid()) {
|
||||
installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS;
|
||||
if (! installed_cache) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message;
|
||||
} else if (! VendorCacheFile::usable_version(cache_in_vendors.string(), bundle).valid()) {
|
||||
// The copy is what will be loaded, so it — not the kilobyte
|
||||
// peek that chose this form — decides whether the profile
|
||||
// beside it can go.
|
||||
BOOST_LOG_TRIVIAL(warning) << "Installed cache for " << bundle << " cannot be read; installing its profile instead";
|
||||
boost::system::error_code ec;
|
||||
fs::remove(cache_in_vendors, ec);
|
||||
installed_cache = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! installed_cache) {
|
||||
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
|
||||
if (cfr != CopyFileResult::SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
|
||||
all_installed = false;
|
||||
continue;
|
||||
}
|
||||
// Only now: an earlier install's cache would shadow this profile,
|
||||
// but removing it before the profile lands would leave neither.
|
||||
boost::system::error_code ec;
|
||||
fs::remove(cache_in_vendors, ec);
|
||||
} else {
|
||||
// Left in place, an earlier install's profile would shadow the cache.
|
||||
boost::system::error_code ec;
|
||||
fs::remove(path_in_vendors, ec);
|
||||
if (ec)
|
||||
BOOST_LOG_TRIVIAL(warning) << "Could not remove the superseded profile " << path_in_vendors.string() << ": " << ec.message();
|
||||
}
|
||||
|
||||
// Copy the vendor directory (if it exists)
|
||||
auto dir_in_rsrc = rsrc_path / bundle;
|
||||
auto dir_in_vendors = vendor_path / bundle;
|
||||
|
||||
if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
|
||||
// Remove existing directory
|
||||
if (fs::exists(dir_in_vendors))
|
||||
fs::remove_all(dir_in_vendors);
|
||||
// Whatever is installed came from an earlier version of this vendor and
|
||||
// would be parsed in place of the one being installed now.
|
||||
if (fs::exists(dir_in_vendors))
|
||||
fs::remove_all(dir_in_vendors);
|
||||
|
||||
if (! installed_cache && fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
|
||||
fs::create_directories(dir_in_vendors);
|
||||
|
||||
// Copy with file filter (same as PresetUpdater::install_bundles_rsrc)
|
||||
@@ -1787,11 +1915,11 @@ bool install_vendor_bundles_from_resources(
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what();
|
||||
return false;
|
||||
all_installed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return all_installed;
|
||||
}
|
||||
|
||||
void save_string_file(const boost::filesystem::path& p, const std::string& str)
|
||||
|
||||
+34
-18
@@ -38,6 +38,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/AuxiliaryDataViewModel.hpp
|
||||
GUI/AuxiliaryDialog.cpp
|
||||
GUI/AuxiliaryDialog.hpp
|
||||
GUI/AVVideoDecoder.cpp
|
||||
GUI/AVVideoDecoder.hpp
|
||||
GUI/Auxiliary.hpp
|
||||
GUI/BackgroundSlicingProcess.cpp
|
||||
GUI/BackgroundSlicingProcess.hpp
|
||||
@@ -361,6 +363,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
|
||||
@@ -615,6 +627,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/WipeTowerDialog.cpp
|
||||
GUI/wxExtensions.cpp
|
||||
GUI/wxExtensions.hpp
|
||||
GUI/wxMediaCtrl3.cpp
|
||||
GUI/wxMediaCtrl3.h
|
||||
plugin/PythonInterpreter.cpp
|
||||
plugin/PythonInterpreter.hpp
|
||||
plugin/PythonPluginBridge.cpp
|
||||
@@ -803,21 +817,8 @@ if (APPLE)
|
||||
GUI/DeepLinkHandlerMac.mm
|
||||
GUI/DeepLinkHandlerMac.h
|
||||
GUI/GUI_UtilsMac.mm
|
||||
GUI/wxMediaCtrl2.mm
|
||||
GUI/wxMediaCtrl2.h
|
||||
)
|
||||
FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration)
|
||||
else ()
|
||||
list(APPEND SLIC3R_GUI_SOURCES
|
||||
GUI/wxMediaCtrl2.cpp
|
||||
GUI/wxMediaCtrl2.h
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
list(APPEND SLIC3R_GUI_SOURCES
|
||||
GUI/Printer/gstbambusrc.c
|
||||
)
|
||||
endif ()
|
||||
|
||||
set(ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY}")
|
||||
@@ -917,6 +918,26 @@ if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY)
|
||||
add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
# Static FFmpeg from the deps install: nothing to bundle into the .app,
|
||||
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil.
|
||||
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
|
||||
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
|
||||
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
|
||||
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
|
||||
message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.")
|
||||
endif ()
|
||||
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
|
||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
|
||||
else ()
|
||||
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
|
||||
libavcodec
|
||||
libswscale
|
||||
libavutil
|
||||
)
|
||||
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
|
||||
endif()
|
||||
|
||||
# We need to implement some hacks for wxWidgets and touch the underlying GTK
|
||||
# layer and sub-libraries. This forces us to use the include locations and
|
||||
# link these libraries.
|
||||
@@ -943,11 +964,6 @@ if (UNIX AND NOT APPLE)
|
||||
target_compile_definitions(libslic3r_gui PRIVATE wxHAVE_GDK_WAYLAND)
|
||||
endif ()
|
||||
|
||||
# We add GStreamer for bambu:/// support.
|
||||
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
|
||||
pkg_check_modules(GST_BASE REQUIRED gstreamer-base-1.0)
|
||||
target_link_libraries(libslic3r_gui ${GSTREAMER_LIBRARIES} ${GST_BASE_LIBRARIES})
|
||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS})
|
||||
endif ()
|
||||
|
||||
# Add a definition so that we can tell we are compiling slic3r.
|
||||
|
||||
@@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot:
|
||||
cfg.models_variants_installed.erase(it ++);
|
||||
else
|
||||
++ it;
|
||||
// Read the active config bundle, parse the config version.
|
||||
PresetBundle bundle;
|
||||
//BBS: change directoties by design
|
||||
//bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
for (const auto &vp : bundle.vendors)
|
||||
if (vp.second.id == cfg.name)
|
||||
cfg.version.config_version = vp.second.config_version;
|
||||
// Orca: the version the vendor is installed at, read from its profile or —
|
||||
// where the cache is the whole installation — from the cache's own stamp.
|
||||
cfg.version.config_version = installed_vendor_version(cfg.name);
|
||||
snapshot.vendor_configs.emplace_back(std::move(cfg));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "AVVideoDecoder.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
}
|
||||
|
||||
AVVideoDecoder::AVVideoDecoder()
|
||||
{
|
||||
codec_ctx_ = avcodec_alloc_context3(nullptr);
|
||||
}
|
||||
|
||||
AVVideoDecoder::~AVVideoDecoder()
|
||||
{
|
||||
if (sws_ctx_)
|
||||
sws_freeContext(sws_ctx_);
|
||||
if (frame_)
|
||||
av_frame_free(&frame_);
|
||||
if (codec_ctx_)
|
||||
avcodec_free_context(&codec_ctx_);
|
||||
}
|
||||
|
||||
int AVVideoDecoder::open(Bambu_StreamInfo const &info)
|
||||
{
|
||||
auto codec_id = info.sub_type == AVC1 ? AV_CODEC_ID_H264 : AV_CODEC_ID_MJPEG;
|
||||
auto codec = avcodec_find_decoder(codec_id);
|
||||
if (codec == nullptr) {
|
||||
fprintf(stderr, "AVVideoDecoder: unsupported codec!\n");
|
||||
return -1; // Codec not found
|
||||
}
|
||||
/* open the coderc */
|
||||
if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) {
|
||||
fprintf(stderr, "AVVideoDecoder: could not open codec\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Allocate an AVFrame structure
|
||||
frame_ = av_frame_alloc();
|
||||
if (frame_ == nullptr)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int AVVideoDecoder::decode(const Bambu_Sample &sample)
|
||||
{
|
||||
int ret = -1;
|
||||
AVPacket *pkt = av_packet_alloc();
|
||||
if (!pkt) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = av_new_packet(pkt, sample.size);
|
||||
if (ret != 0) {
|
||||
av_packet_free(&pkt);
|
||||
return ret;
|
||||
}
|
||||
|
||||
memcpy(pkt->data, sample.buffer, size_t(sample.size));
|
||||
|
||||
ret = avcodec_send_packet(codec_ctx_, pkt);
|
||||
if (ret == 0) {
|
||||
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
|
||||
}
|
||||
|
||||
av_packet_unref(pkt);
|
||||
av_packet_free(&pkt);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int AVVideoDecoder::flush()
|
||||
{
|
||||
int ret = avcodec_send_packet(codec_ctx_, nullptr);
|
||||
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AVVideoDecoder::close()
|
||||
{
|
||||
}
|
||||
|
||||
bool AVVideoDecoder::toWxImage(wxImage &image, wxSize const &size2)
|
||||
{
|
||||
if (!got_frame_)
|
||||
return false;
|
||||
|
||||
auto size1 = size2;
|
||||
if (!size1.IsFullySpecified())
|
||||
size1 = {frame_->width, frame_->height };
|
||||
auto size = size1;
|
||||
if (size.GetWidth() & 0x0f) {
|
||||
size.SetWidth((size.GetWidth() & ~0x0f) + 0x10);
|
||||
if (size.GetWidth() != width_) {
|
||||
std::fill(bits_.begin(), bits_.end(), 0);
|
||||
width_ = size.GetWidth();
|
||||
}
|
||||
}
|
||||
AVPixelFormat wxFmt = AV_PIX_FMT_RGB24;
|
||||
sws_ctx_ = sws_getCachedContext(sws_ctx_,
|
||||
frame_->width, frame_->height, AVPixelFormat(frame_->format),
|
||||
size1.GetWidth(), size1.GetHeight(), wxFmt,
|
||||
SWS_GAUSS,
|
||||
nullptr, nullptr, nullptr);
|
||||
if (sws_ctx_ == nullptr)
|
||||
return false;
|
||||
int length = size.GetWidth() * size.GetHeight() * 3;
|
||||
if (bits_.size() < length)
|
||||
bits_.resize(length);
|
||||
uint8_t * datas[] = { bits_.data() };
|
||||
int strides[] = { size.GetWidth() * 3 };
|
||||
int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides);
|
||||
if (result_h != size.GetHeight()) {
|
||||
return false;
|
||||
}
|
||||
// Copy: the frame outlives this decoder and is painted by the GUI thread while the
|
||||
// next sws_scale is already overwriting bits_, so it must own its pixels. The Windows
|
||||
// path below needs no equivalent, wxBitmap copies the bits into GDI.
|
||||
image = wxImage(size.GetWidth(), size.GetHeight(), bits_.data(), true).Copy();
|
||||
if (!image.IsOk()) {
|
||||
fprintf(stderr, "AVVideoDecoder: image not ok %dx%d\n", size.GetWidth(), size.GetHeight());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AVVideoDecoder::toWxBitmap(wxBitmap &bitmap, wxSize const &size2)
|
||||
{
|
||||
if (!got_frame_)
|
||||
return false;
|
||||
|
||||
auto size1 = size2;
|
||||
if (!size1.IsFullySpecified())
|
||||
size1 = {frame_->width, frame_->height };
|
||||
auto size = size1;
|
||||
if (size.GetWidth() & 0x0f) {
|
||||
size.SetWidth((size.GetWidth() & ~0x0f) + 0x10);
|
||||
if (size.GetWidth() != width_) {
|
||||
std::fill(bits_.begin(), bits_.end(), 0);
|
||||
width_ = size.GetWidth();
|
||||
}
|
||||
}
|
||||
AVPixelFormat wxFmt = AV_PIX_FMT_RGB32;
|
||||
sws_ctx_ = sws_getCachedContext(sws_ctx_,
|
||||
frame_->width, frame_->height, AVPixelFormat(frame_->format),
|
||||
size1.GetWidth(), size1.GetHeight(), wxFmt,
|
||||
SWS_GAUSS,
|
||||
nullptr, nullptr, nullptr);
|
||||
if (sws_ctx_ == nullptr)
|
||||
return false;
|
||||
int length = size.GetWidth() * size.GetHeight() * 4;
|
||||
if (bits_.size() < length)
|
||||
bits_.resize(length);
|
||||
uint8_t *datas[] = { bits_.data() };
|
||||
int strides[] = { size.GetWidth() * 4 };
|
||||
int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides);
|
||||
if (result_h != size.GetHeight()) {
|
||||
fprintf(stderr, "AVVideoDecoder: result_h %d %d\n", result_h, size.GetHeight());
|
||||
return false;
|
||||
}
|
||||
bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32);
|
||||
assert(bitmap.IsOk());
|
||||
if (!bitmap.IsOk()) {
|
||||
fprintf(stderr, "AVVideoDecoder: bitmap not ok %dx%d\n", size.GetWidth(), size.GetHeight());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef AVVIDEODECODER_HPP
|
||||
#define AVVIDEODECODER_HPP
|
||||
|
||||
#include "Printer/BambuTunnel.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libswscale/swscale.h>
|
||||
}
|
||||
#include <vector>
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/image.h>
|
||||
|
||||
class wxBitmap;
|
||||
|
||||
class AVVideoDecoder
|
||||
{
|
||||
public:
|
||||
AVVideoDecoder();
|
||||
|
||||
~AVVideoDecoder();
|
||||
|
||||
public:
|
||||
int open(Bambu_StreamInfo const &info);
|
||||
|
||||
int decode(Bambu_Sample const &sample);
|
||||
|
||||
int flush();
|
||||
|
||||
void close();
|
||||
|
||||
bool toWxImage(wxImage &image, wxSize const &size);
|
||||
|
||||
bool toWxBitmap(wxBitmap &bitmap, wxSize const & size);
|
||||
|
||||
private:
|
||||
AVCodecContext *codec_ctx_ = nullptr;
|
||||
AVFrame * frame_ = nullptr;
|
||||
SwsContext * sws_ctx_ = nullptr;
|
||||
bool got_frame_ = false;
|
||||
int width_ { 0 }; // scale result width
|
||||
std::vector<uint8_t> bits_;
|
||||
};
|
||||
|
||||
#endif // AVVIDEODECODER_HPP
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// BambuPlayer.h
|
||||
// BambuPlayer
|
||||
//
|
||||
// Created by cmguo on 2021/12/6.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFoundation/AVSampleBufferDisplayLayer.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BambuPlayer : NSObject
|
||||
|
||||
+ (void) initialize;
|
||||
|
||||
- (instancetype) initWithDisplayLayer: (AVSampleBufferDisplayLayer*) layer;
|
||||
- (instancetype) initWithImageView: (NSView*) view;
|
||||
- (int) open: (char const *) url;
|
||||
- (NSSize) videoSize;
|
||||
- (int) play;
|
||||
- (void) stop;
|
||||
- (void) close;
|
||||
|
||||
- (void) setLogger: (void (*)(void const * context, int level, char const * msg)) logger withContext: (void const *) context;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1015,7 +1015,7 @@ wxBoxSizer* CalibrationPresetPage::create_ams_items_sizer(MachineObject* obj, wx
|
||||
auto ams_items_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
for (auto &info : ams_info) {
|
||||
auto preview_ams_item = new AMSPreview(ams_preview_panel, wxID_ANY, info, info.ams_type);
|
||||
preview_ams_item->Update(info);
|
||||
preview_ams_item->UpdateInfo(info);
|
||||
preview_ams_item->Open();
|
||||
ams_preview_list.push_back(preview_ams_item);
|
||||
std::string ams_id = preview_ams_item->get_ams_id();
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user