diff --git a/.gitignore b/.gitignore index 4d3ccb5c7b..cdcd1c90b4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ Build Build.bat /build*/ CMakeLists.txt.user +CMakeUserPresets.json **/CMakeLists.txt.autosave deps/build* MYMETA.json diff --git a/deps/Assimp/Assimp.cmake b/deps/Assimp/Assimp.cmake new file mode 100644 index 0000000000..8b4de03b09 --- /dev/null +++ b/deps/Assimp/Assimp.cmake @@ -0,0 +1,40 @@ +if(CMAKE_VERSION VERSION_LESS 3.22) + set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.3.1.tar.gz") + set(_assimp_hash "SHA256=a07666be71afe1ad4bc008c2336b7c688aca391271188eb9108d0c6db1be53f1") +else() + set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz") + set(_assimp_hash "SHA256=66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb") +endif() + +# Assimp's bundled zlib (contrib/zlib) is too old to compile against the modern +# macOS SDK: its zutil.h takes the classic-Mac branch under TARGET_OS_MAC and +# does `#define fdopen(fd,mode) NULL`, which then clobbers the SDK's real +# `fdopen` prototype in and breaks the build. On macOS use the system +# zlib (already found by find_package(ZLIB) in deps-unix-common) instead. +if(APPLE) + set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=OFF") +else() + set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=ON") +endif() + +orcaslicer_add_cmake_project(Assimp + URL ${_assimp_url} + URL_HASH ${_assimp_hash} + CMAKE_ARGS + -DASSIMP_BUILD_TESTS=OFF + -DASSIMP_BUILD_SAMPLES=OFF + -DASSIMP_BUILD_ASSIMP_TOOLS=OFF + -DASSIMP_INSTALL_PDB=OFF + -DASSIMP_NO_EXPORT=ON + -DASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT=OFF + -DASSIMP_BUILD_GLTF_IMPORTER=ON + -DASSIMP_BUILD_OBJ_IMPORTER=ON + -DASSIMP_BUILD_FBX_IMPORTER=ON + ${_assimp_build_zlib} + -DASSIMP_WARNINGS_AS_ERRORS=OFF + -DBUILD_WITH_STATIC_CRT=OFF +) + +if (MSVC) + add_debug_dep(dep_Assimp) +endif () diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 39cc5de182..8f4bc2a215 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -367,6 +367,9 @@ include(libnoise/libnoise.cmake) include(Draco/Draco.cmake) +# Assimp: glTF/GLB/FBX import for the texture-to-color feature. +include(Assimp/Assimp.cmake) + # I *think* 1.1 is used for *just* md5 hashing? # 3.1 has everything in the right place, but the md5 funcs used are deprecated @@ -448,6 +451,7 @@ set(_dep_list dep_libnoise dep_python3 dep_wxInspector + dep_Assimp ) if (MSVC) diff --git a/resources/filament_mixing/standard_color_recipes.json b/resources/filament_mixing/standard_color_recipes.json new file mode 100644 index 0000000000..df03b49ac3 --- /dev/null +++ b/resources/filament_mixing/standard_color_recipes.json @@ -0,0 +1,14705 @@ +{ + "_comment": "Simulated values (source=filament_mixer) are generated by FilamentMixer, a degree-4 polynomial regression trained to approximate Mixbox behavior (Mean Delta-E ~2.07). This file does not use Mixbox source code, binaries, or data files. See src/libslic3r/FilamentMixerModel.hpp.", + "entries": [ + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 48.14, + 33.87, + -25.42 + ], + "measured_rgb": "#965E9E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 48.2, + 33.11, + -25.85 + ], + "measured_rgb": "#955F9E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 48.06, + 29.44, + -27.62 + ], + "measured_rgb": "#8D61A1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 47.82, + 28.45, + -28.19 + ], + "measured_rgb": "#8A62A1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 47.94, + 22.08, + -30.15 + ], + "measured_rgb": "#7D67A5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 47.64, + 24.63, + -30.31 + ], + "measured_rgb": "#8164A4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 48.28, + 17.77, + -31.76 + ], + "measured_rgb": "#746BA8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 48.15, + 16.73, + -32.42 + ], + "measured_rgb": "#706BA9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 48.57, + 17.11, + -32.94 + ], + "measured_rgb": "#716CAB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 48.95, + 13.88, + -33.83 + ], + "measured_rgb": "#6A6FAE", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 49.1, + 13.76, + -34.39 + ], + "measured_rgb": "#6A70AF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 49.58, + 11.74, + -35.13 + ], + "measured_rgb": "#6572B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 50.88, + 5.61, + -36.4 + ], + "measured_rgb": "#5679B7", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 70.0, + -35.0, + 56.27 + ], + "measured_rgb": "#8ABA3C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 69.5, + -36.46, + 55.41 + ], + "measured_rgb": "#85B93C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 67.44, + -38.62, + 50.18 + ], + "measured_rgb": "#78B443", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 66.34, + -39.72, + 47.6 + ], + "measured_rgb": "#71B246", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 65.4, + -40.42, + 42.8 + ], + "measured_rgb": "#6AB04E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 63.51, + -42.24, + 38.44 + ], + "measured_rgb": "#5DAB52", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 63.04, + -42.17, + 35.8 + ], + "measured_rgb": "#59AA56", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 62.21, + -43.03, + 32.64 + ], + "measured_rgb": "#51A85A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 60.68, + -43.94, + 27.49 + ], + "measured_rgb": "#44A560", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 60.36, + -43.76, + 23.75 + ], + "measured_rgb": "#3FA466", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 59.06, + -44.3, + 17.42 + ], + "measured_rgb": "#2CA16E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 58.47, + -44.1, + 14.26 + ], + "measured_rgb": "#219F72", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 58.09, + -43.7, + 10.53 + ], + "measured_rgb": "#139E78", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 73.74, + -14.74, + -21.41 + ], + "measured_rgb": "#78BFDC", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 71.76, + -15.4, + -24.59 + ], + "measured_rgb": "#69BADC", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 70.11, + -15.73, + -25.86 + ], + "measured_rgb": "#60B6DA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 68.1, + -16.03, + -28.26 + ], + "measured_rgb": "#53B1D8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 67.93, + -15.44, + -28.32 + ], + "measured_rgb": "#55B0D8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 66.5, + -15.79, + -29.78 + ], + "measured_rgb": "#4AACD6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 65.75, + -15.69, + -30.79 + ], + "measured_rgb": "#44AAD6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 64.67, + -15.68, + -31.78 + ], + "measured_rgb": "#3DA8D5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 62.88, + -16.04, + -34.06 + ], + "measured_rgb": "#27A3D4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 62.86, + -15.71, + -34.68 + ], + "measured_rgb": "#26A3D5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 62.09, + -15.68, + -35.76 + ], + "measured_rgb": "#19A1D5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 60.73, + -15.52, + -36.7 + ], + "measured_rgb": "#009DD3", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 60.03, + -15.72, + -37.19 + ], + "measured_rgb": "#009CD1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 65.7, + 19.56, + 48.15 + ], + "measured_rgb": "#D69148", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 63.26, + 25.13, + 42.51 + ], + "measured_rgb": "#D5864E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 60.24, + 30.06, + 32.69 + ], + "measured_rgb": "#D17B59", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 60.41, + 29.22, + 34.96 + ], + "measured_rgb": "#D17C55", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 57.97, + 35.04, + 26.17 + ], + "measured_rgb": "#CF7160", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 57.16, + 35.32, + 24.58 + ], + "measured_rgb": "#CC6F60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 57.46, + 36.08, + 25.44 + ], + "measured_rgb": "#CE6F60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 56.02, + 38.35, + 20.53 + ], + "measured_rgb": "#CC6965", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 55.1, + 39.69, + 16.16 + ], + "measured_rgb": "#C9666A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 54.88, + 41.31, + 15.93 + ], + "measured_rgb": "#CB646A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 53.45, + 45.1, + 7.0 + ], + "measured_rgb": "#C85D76", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 53.23, + 44.29, + 7.96 + ], + "measured_rgb": "#C75D73", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 53.17, + 45.44, + 5.5 + ], + "measured_rgb": "#C75C77", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 65.05, + 41.46, + -15.23 + ], + "measured_rgb": "#D981BA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 64.76, + 41.42, + -14.99 + ], + "measured_rgb": "#D881B9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 61.49, + 47.18, + -15.56 + ], + "measured_rgb": "#D773B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 62.17, + 44.07, + -15.56 + ], + "measured_rgb": "#D577B3", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 61.67, + 45.03, + -15.24 + ], + "measured_rgb": "#D575B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 60.38, + 47.58, + -15.12 + ], + "measured_rgb": "#D56FAD", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 57.67, + 51.19, + -15.52 + ], + "measured_rgb": "#D264A7", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 57.19, + 52.57, + -15.0 + ], + "measured_rgb": "#D361A5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 57.75, + 52.16, + -14.62 + ], + "measured_rgb": "#D463A6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 57.2, + 51.19, + -14.81 + ], + "measured_rgb": "#D163A4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 55.57, + 53.66, + -14.82 + ], + "measured_rgb": "#D05BA0", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 55.51, + 53.08, + -14.46 + ], + "measured_rgb": "#CF5C9F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 54.62, + 54.16, + -14.51 + ], + "measured_rgb": "#CE589D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 87.72, + -15.64, + 55.43 + ], + "measured_rgb": "#E1E26F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 87.51, + -15.48, + 58.85 + ], + "measured_rgb": "#E2E167", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 87.35, + -15.37, + 61.45 + ], + "measured_rgb": "#E3E161", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 87.0, + -14.73, + 63.73 + ], + "measured_rgb": "#E4DF5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 86.76, + -14.25, + 65.47 + ], + "measured_rgb": "#E4DE56", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 86.6, + -13.9, + 67.58 + ], + "measured_rgb": "#E5DD50", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 86.23, + -14.03, + 72.5 + ], + "measured_rgb": "#E5DC42", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 86.91, + -12.84, + 74.11 + ], + "measured_rgb": "#EADE3F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 86.24, + -13.03, + 75.23 + ], + "measured_rgb": "#E8DC3A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 86.01, + -12.73, + 76.77 + ], + "measured_rgb": "#E8DB34", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 85.85, + -12.44, + 78.22 + ], + "measured_rgb": "#E8DA2E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 85.71, + -12.39, + 81.24 + ], + "measured_rgb": "#E9DA21", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 87.04, + -10.65, + 83.79 + ], + "measured_rgb": "#F0DC1A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 57.59, + -7.31, + 27.59 + ], + "measured_rgb": "#8F8D5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 56.47, + -5.15, + 29.22 + ], + "measured_rgb": "#908954", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 53.76, + 1.47, + 16.52 + ], + "measured_rgb": "#8E7F64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 53.89, + 1.26, + 22.63 + ], + "measured_rgb": "#917F5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 53.08, + 4.21, + 20.07 + ], + "measured_rgb": "#927B5D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 51.5, + 7.8, + 12.87 + ], + "measured_rgb": "#907565", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 50.11, + 12.7, + 3.91 + ], + "measured_rgb": "#8F6F71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 49.99, + 13.42, + 6.09 + ], + "measured_rgb": "#916F6D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 49.64, + 14.45, + 6.31 + ], + "measured_rgb": "#926D6C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 57.53, + -11.27, + 27.15 + ], + "measured_rgb": "#888F5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 54.99, + -7.03, + 22.56 + ], + "measured_rgb": "#86865C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 54.25, + -4.47, + 21.25 + ], + "measured_rgb": "#88835D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 53.04, + -2.53, + 17.95 + ], + "measured_rgb": "#867F60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 51.71, + 2.72, + 11.95 + ], + "measured_rgb": "#887967", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 50.85, + 5.89, + 10.93 + ], + "measured_rgb": "#8A7567", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 49.74, + 8.68, + 4.66 + ], + "measured_rgb": "#88716F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 49.3, + 9.76, + 2.9 + ], + "measured_rgb": "#886F71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 55.45, + -14.4, + 18.45 + ], + "measured_rgb": "#788B64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 55.07, + -10.24, + 24.11 + ], + "measured_rgb": "#82885A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 53.44, + -6.68, + 18.83 + ], + "measured_rgb": "#81825F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 52.37, + -3.05, + 13.89 + ], + "measured_rgb": "#817E65", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 51.03, + 0.35, + 11.39 + ], + "measured_rgb": "#827966", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 50.12, + 4.03, + 7.42 + ], + "measured_rgb": "#83756B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 49.36, + 7.35, + 3.42 + ], + "measured_rgb": "#847170", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 55.52, + -16.34, + 20.5 + ], + "measured_rgb": "#758C61", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 53.19, + -11.0, + 14.49 + ], + "measured_rgb": "#768466", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.8, + -8.48, + 16.8 + ], + "measured_rgb": "#7D8463", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 51.71, + -4.82, + 12.37 + ], + "measured_rgb": "#7C7D66", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 49.92, + 7.98, + 2.05 + ], + "measured_rgb": "#867274", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 48.86, + 10.46, + 0.0 + ], + "measured_rgb": "#866E74", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 54.4, + -10.2, + 19.17 + ], + "measured_rgb": "#7D8661", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 52.52, + -4.36, + 12.99 + ], + "measured_rgb": "#7F7F67", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 51.21, + -1.28, + 6.49 + ], + "measured_rgb": "#7D7A6F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 50.18, + 4.0, + 4.31 + ], + "measured_rgb": "#817570", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 48.97, + 7.15, + 1.93 + ], + "measured_rgb": "#827071", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 52.71, + -11.69, + 14.25 + ], + "measured_rgb": "#738365", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 51.33, + -3.15, + 5.87 + ], + "measured_rgb": "#797C70", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 49.87, + -1.14, + 1.5 + ], + "measured_rgb": "#767774", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 50.92, + -2.35, + 7.7 + ], + "measured_rgb": "#7B7A6C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 52.42, + -12.64, + 11.74 + ], + "measured_rgb": "#6E8369", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 50.67, + -4.67, + -2.82 + ], + "measured_rgb": "#6D7B7D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 49.65, + -0.75, + -0.75 + ], + "measured_rgb": "#747677", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 52.24, + -12.06, + 7.79 + ], + "measured_rgb": "#6C826F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 50.78, + -8.82, + 4.42 + ], + "measured_rgb": "#6C7D71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 52.04, + -14.16, + -0.63 + ], + "measured_rgb": "#5F837D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 60.62, + 16.41, + -27.19 + ], + "measured_rgb": "#978BC2", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 58.416, + 18.423, + -27.936 + ], + "measured_rgb": "#9484BD", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 56.19, + 22.63, + -28.47 + ], + "measured_rgb": "#967BB8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 55.817, + 22.511, + -27.942 + ], + "measured_rgb": "#957AB6", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 56.48, + 23.36, + -26.2 + ], + "measured_rgb": "#9A7BB5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 54.683, + 24.228, + -27.087 + ], + "measured_rgb": "#9676B2", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 53.86, + 25.93, + -26.83 + ], + "measured_rgb": "#9773AF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 52.924, + 27.03, + -27.166 + ], + "measured_rgb": "#966FAD", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 51.71, + 29.47, + -27.22 + ], + "measured_rgb": "#976AAA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 60.566, + 13.416, + -27.029 + ], + "measured_rgb": "#918CC2", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 58.437, + 16.227, + -28.148 + ], + "measured_rgb": "#9085BE", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 55.09, + 20.811, + -29.603 + ], + "measured_rgb": "#8E7AB7", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 54.78, + 21.542, + -29.157 + ], + "measured_rgb": "#8F78B6", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 54.433, + 22.913, + -28.35 + ], + "measured_rgb": "#9276B3", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 53.707, + 23.395, + -28.23 + ], + "measured_rgb": "#9174B1", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 53.303, + 23.898, + -28.057 + ], + "measured_rgb": "#9173B0", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 52.606, + 25.673, + -27.911 + ], + "measured_rgb": "#9270AE", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 62.64, + 7.61, + -25.75 + ], + "measured_rgb": "#8C95C5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 57.776, + 12.715, + -29.283 + ], + "measured_rgb": "#8586BE", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 54.3, + 18.26, + -31.18 + ], + "measured_rgb": "#857AB8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 53.449, + 19.947, + -30.78 + ], + "measured_rgb": "#8776B5", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 52.15, + 21.92, + -30.78 + ], + "measured_rgb": "#8772B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 52.733, + 22.562, + -29.373 + ], + "measured_rgb": "#8B72B0", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 52.34, + 22.37, + -29.11 + ], + "measured_rgb": "#8A72AF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 58.326, + 9.338, + -30.016 + ], + "measured_rgb": "#7E89C1", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 56.387, + 12.275, + -30.918 + ], + "measured_rgb": "#7F83BD", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.575, + 16.404, + -32.238 + ], + "measured_rgb": "#7E79B8", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 52.46, + 18.41, + -32.043 + ], + "measured_rgb": "#7F75B4", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 50.49, + 23.51, + -31.23 + ], + "measured_rgb": "#856CAE", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 49.65, + 26.8, + -30.56 + ], + "measured_rgb": "#8A68AA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 55.49, + 12.01, + -31.53 + ], + "measured_rgb": "#7B81BB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 54.0, + 18.26, + -30.55 + ], + "measured_rgb": "#8579B6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 52.11, + 16.18, + -32.9 + ], + "measured_rgb": "#7976B5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 50.58, + 22.22, + -31.81 + ], + "measured_rgb": "#826EAF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 49.52, + 22.45, + -31.95 + ], + "measured_rgb": "#806BAC", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 55.21, + 10.3, + -31.74 + ], + "measured_rgb": "#7681BB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 53.01, + 17.69, + -31.28 + ], + "measured_rgb": "#8077B4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 51.1, + 17.85, + -33.01 + ], + "measured_rgb": "#7972B2", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 50.31, + 22.05, + -31.57 + ], + "measured_rgb": "#816DAE", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 53.39, + 8.16, + -34.46 + ], + "measured_rgb": "#687EBB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 52.44, + 17.84, + -31.9 + ], + "measured_rgb": "#7E75B4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 50.6, + 16.89, + -33.72 + ], + "measured_rgb": "#7571B2", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 53.48, + 9.63, + -33.29 + ], + "measured_rgb": "#6D7DB9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 51.33, + 13.07, + -34.41 + ], + "measured_rgb": "#6E76B5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 52.25, + 10.16, + -35.14 + ], + "measured_rgb": "#687AB9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 74.3, + -35.02, + 32.23 + ], + "measured_rgb": "#87C77A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 73.72, + -36.25, + 35.28 + ], + "measured_rgb": "#85C572", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 73.49, + -37.39, + 44.85 + ], + "measured_rgb": "#88C55E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 73.18, + -37.11, + 44.81 + ], + "measured_rgb": "#88C45E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 73.27, + -36.92, + 47.03 + ], + "measured_rgb": "#8AC459", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 72.96, + -36.08, + 48.85 + ], + "measured_rgb": "#8CC355", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 72.86, + -36.19, + 52.04 + ], + "measured_rgb": "#8DC24D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 72.69, + -36.31, + 54.47 + ], + "measured_rgb": "#8EC247", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 73.02, + -34.99, + 57.46 + ], + "measured_rgb": "#93C241", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 72.41, + -36.38, + 27.96 + ], + "measured_rgb": "#7BC27D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 71.93, + -38.31, + 37.24 + ], + "measured_rgb": "#7DC16A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 72.08, + -39.6, + 45.03 + ], + "measured_rgb": "#7FC25A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 71.28, + -39.24, + 43.36 + ], + "measured_rgb": "#7DC05C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 71.07, + -38.65, + 44.35 + ], + "measured_rgb": "#7EBF59", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 71.18, + -38.31, + 50.43 + ], + "measured_rgb": "#83BF4C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 70.55, + -38.48, + 48.2 + ], + "measured_rgb": "#80BD50", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 71.23, + -37.53, + 52.06 + ], + "measured_rgb": "#86BE49", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 70.62, + -38.31, + 27.17 + ], + "measured_rgb": "#70BE7A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 70.27, + -40.7, + 36.95 + ], + "measured_rgb": "#72BE66", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 69.68, + -39.3, + 32.01 + ], + "measured_rgb": "#70BB6E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 69.43, + -40.46, + 39.0 + ], + "measured_rgb": "#72BB60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 68.97, + -40.7, + 38.6 + ], + "measured_rgb": "#70BA5F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 69.63, + -40.15, + 47.16 + ], + "measured_rgb": "#79BB4F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 69.41, + -39.15, + 50.18 + ], + "measured_rgb": "#7CBA48", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 68.36, + -39.74, + 23.12 + ], + "measured_rgb": "#62B87B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 68.0, + -41.34, + 28.71 + ], + "measured_rgb": "#62B870", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 67.59, + -41.06, + 29.34 + ], + "measured_rgb": "#63B76E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 67.99, + -40.51, + 34.43 + ], + "measured_rgb": "#6AB765", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 68.47, + -41.01, + 39.93 + ], + "measured_rgb": "#6EB95B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 68.39, + -40.84, + 44.81 + ], + "measured_rgb": "#72B851", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 68.26, + -42.84, + 32.38 + ], + "measured_rgb": "#62B96A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 67.53, + -43.05, + 33.52 + ], + "measured_rgb": "#61B765", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 67.97, + -43.08, + 40.92 + ], + "measured_rgb": "#68B858", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 67.12, + -42.36, + 35.29 + ], + "measured_rgb": "#63B661", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 67.2, + -42.55, + 42.58 + ], + "measured_rgb": "#69B653", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 66.46, + -43.54, + 30.78 + ], + "measured_rgb": "#5AB468", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 67.09, + -43.25, + 40.33 + ], + "measured_rgb": "#65B657", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 66.18, + -43.88, + 36.56 + ], + "measured_rgb": "#5EB35C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 66.19, + -43.23, + 41.16 + ], + "measured_rgb": "#63B353", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 64.97, + -43.99, + 23.68 + ], + "measured_rgb": "#4BB172", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 65.56, + -44.13, + 36.03 + ], + "measured_rgb": "#5BB25C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 64.72, + -44.5, + 33.55 + ], + "measured_rgb": "#55B05E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 64.39, + -44.62, + 30.57 + ], + "measured_rgb": "#50AF63", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 64.28, + -44.35, + 33.12 + ], + "measured_rgb": "#54AF5E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 63.99, + -44.5, + 31.36 + ], + "measured_rgb": "#50AE61", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 69.61, + 16.79, + 31.35 + ], + "measured_rgb": "#D99E72", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 68.82, + 19.72, + 31.9 + ], + "measured_rgb": "#DB996F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 68.33, + 17.1, + 39.2 + ], + "measured_rgb": "#D89A60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 68.46, + 16.11, + 42.41 + ], + "measured_rgb": "#D89B5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 68.13, + 20.07, + 35.11 + ], + "measured_rgb": "#DA9768", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 67.98, + 17.82, + 41.09 + ], + "measured_rgb": "#D9985C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 67.42, + 17.87, + 44.12 + ], + "measured_rgb": "#D89654", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 67.92, + 16.3, + 49.75 + ], + "measured_rgb": "#D9994A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 67.28, + 19.02, + 46.35 + ], + "measured_rgb": "#DA9550", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 67.5, + 21.75, + 22.94 + ], + "measured_rgb": "#D7957C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 67.1, + 22.38, + 25.65 + ], + "measured_rgb": "#D79376", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 66.59, + 19.58, + 37.69 + ], + "measured_rgb": "#D6935F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 65.65, + 23.82, + 31.58 + ], + "measured_rgb": "#D78E68", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 65.85, + 22.45, + 35.34 + ], + "measured_rgb": "#D78F62", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 66.33, + 18.54, + 46.19 + ], + "measured_rgb": "#D6934E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 66.54, + 17.79, + 48.57 + ], + "measured_rgb": "#D69449", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 66.05, + 22.67, + 41.69 + ], + "measured_rgb": "#DA8F56", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 65.98, + 25.27, + 25.85 + ], + "measured_rgb": "#D98E73", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 64.68, + 23.73, + 29.99 + ], + "measured_rgb": "#D48C69", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 65.13, + 22.9, + 33.41 + ], + "measured_rgb": "#D58D63", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 65.09, + 25.37, + 33.28 + ], + "measured_rgb": "#D98B64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 63.98, + 23.77, + 34.82 + ], + "measured_rgb": "#D48A5E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 63.93, + 23.89, + 38.63 + ], + "measured_rgb": "#D58957", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 63.96, + 22.94, + 39.59 + ], + "measured_rgb": "#D48A55", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 64.24, + 30.82, + 10.43 + ], + "measured_rgb": "#D5868B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 63.91, + 27.46, + 22.59 + ], + "measured_rgb": "#D48774", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 62.99, + 27.33, + 27.98 + ], + "measured_rgb": "#D38568", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 63.4, + 25.17, + 33.91 + ], + "measured_rgb": "#D4875E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 61.18, + 29.42, + 27.72 + ], + "measured_rgb": "#D17E64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 59.89, + 31.27, + 25.11 + ], + "measured_rgb": "#CF7966", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 59.5, + 33.72, + 17.49 + ], + "measured_rgb": "#CF7772", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 59.49, + 33.87, + 17.34 + ], + "measured_rgb": "#CF7773", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 59.88, + 32.77, + 22.0 + ], + "measured_rgb": "#D0786B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 58.58, + 33.89, + 21.05 + ], + "measured_rgb": "#CD746A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 59.5, + 31.18, + 26.51 + ], + "measured_rgb": "#CE7862", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 58.96, + 35.14, + 15.3 + ], + "measured_rgb": "#CE7475", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 58.15, + 35.31, + 18.11 + ], + "measured_rgb": "#CD726E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 58.1, + 36.43, + 15.98 + ], + "measured_rgb": "#CE7172", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 57.38, + 34.94, + 21.31 + ], + "measured_rgb": "#CB7066", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 57.08, + 37.78, + 13.15 + ], + "measured_rgb": "#CB6D74", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 56.95, + 38.09, + 14.5 + ], + "measured_rgb": "#CC6D71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 57.03, + 37.09, + 17.89 + ], + "measured_rgb": "#CC6D6C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 56.59, + 40.06, + 11.45 + ], + "measured_rgb": "#CC6A76", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 56.35, + 39.5, + 14.12 + ], + "measured_rgb": "#CC6A71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 56.79, + 44.06, + 0.12 + ], + "measured_rgb": "#CE688A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 60.61, + 34.14, + 50.34 + ], + "measured_rgb": "#DB7839", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 57.92, + 38.03, + 46.93 + ], + "measured_rgb": "#D86D39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 54.45, + 45.01, + 43.39 + ], + "measured_rgb": "#D55D39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 52.18, + 47.64, + 41.46 + ], + "measured_rgb": "#D15537", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 54.08, + 42.25, + 42.36 + ], + "measured_rgb": "#D05F3A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 51.19, + 47.93, + 39.54 + ], + "measured_rgb": "#CE5239", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 49.49, + 50.76, + 38.06 + ], + "measured_rgb": "#CC4A38", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 50.15, + 48.98, + 38.57 + ], + "measured_rgb": "#CC4E38", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 49.79, + 47.23, + 37.84 + ], + "measured_rgb": "#C94F39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 46.81, + 51.92, + 34.7 + ], + "measured_rgb": "#C54138", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 46.58, + 51.44, + 34.56 + ], + "measured_rgb": "#C34137", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 46.73, + 51.61, + 33.68 + ], + "measured_rgb": "#C44139", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 46.44, + 51.41, + 32.33 + ], + "measured_rgb": "#C3413B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 28.39, + 4.73, + -17.78 + ], + "measured_rgb": "#3A425E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 27.99, + 5.7, + -13.75 + ], + "measured_rgb": "#404057", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 28.01, + 5.88, + -12.79 + ], + "measured_rgb": "#414056", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 28.36, + 7.17, + -7.97 + ], + "measured_rgb": "#48404F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 28.46, + 8.34, + -6.09 + ], + "measured_rgb": "#4C3F4D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 29.18, + 7.99, + -6.06 + ], + "measured_rgb": "#4D414E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 29.47, + 9.68, + -3.27 + ], + "measured_rgb": "#52404B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 29.14, + 11.96, + -0.91 + ], + "measured_rgb": "#563E46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 29.79, + 10.84, + -2.53 + ], + "measured_rgb": "#55404A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 29.84, + 13.65, + 0.54 + ], + "measured_rgb": "#5B3F46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 30.46, + 16.09, + 2.93 + ], + "measured_rgb": "#613E44", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 31.4, + 20.19, + 7.06 + ], + "measured_rgb": "#6B3D40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 32.0, + 21.78, + 8.02 + ], + "measured_rgb": "#6E3D40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 61.58, + 37.37, + 13.91 + ], + "measured_rgb": "#D8797E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 60.1, + 40.18, + 16.87 + ], + "measured_rgb": "#D97375", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 59.12, + 40.19, + 17.09 + ], + "measured_rgb": "#D67072", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 56.42, + 42.46, + 18.74 + ], + "measured_rgb": "#D26769", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 54.17, + 44.16, + 20.38 + ], + "measured_rgb": "#CE5F61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 53.53, + 44.14, + 20.39 + ], + "measured_rgb": "#CC5D5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 55.16, + 41.55, + 17.38 + ], + "measured_rgb": "#CC6468", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 52.54, + 43.49, + 19.07 + ], + "measured_rgb": "#C85B5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 52.43, + 43.2, + 19.01 + ], + "measured_rgb": "#C75B5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 49.22, + 47.26, + 24.6 + ], + "measured_rgb": "#C44E4E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 48.94, + 47.01, + 23.99 + ], + "measured_rgb": "#C34E4E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 48.65, + 46.66, + 23.5 + ], + "measured_rgb": "#C14D4F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 46.24, + 48.73, + 26.28 + ], + "measured_rgb": "#BD4444", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 35.03, + -18.52, + -8.74 + ], + "measured_rgb": "#185B60", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 34.35, + -18.03, + -10.06 + ], + "measured_rgb": "#145960", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 36.58, + -19.91, + -5.37 + ], + "measured_rgb": "#205F5E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 40.4, + -23.93, + 5.19 + ], + "measured_rgb": "#306956", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 40.47, + -25.72, + 6.68 + ], + "measured_rgb": "#2D6A54", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 42.28, + -26.69, + 10.44 + ], + "measured_rgb": "#346F52", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 42.84, + -27.22, + 11.67 + ], + "measured_rgb": "#357051", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 45.25, + -27.6, + 15.74 + ], + "measured_rgb": "#3F7750", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 48.65, + -28.39, + 22.55 + ], + "measured_rgb": "#4C7F4C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 50.85, + -29.47, + 27.4 + ], + "measured_rgb": "#538549", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 54.25, + -29.09, + 32.92 + ], + "measured_rgb": "#608E46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 49.95, + -30.96, + 23.7 + ], + "measured_rgb": "#4A844D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 59.02, + -29.15, + 40.67 + ], + "measured_rgb": "#719A43", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 86.91, + -14.57, + 48.46 + ], + "measured_rgb": "#DDDF7B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 86.88, + -15.04, + 54.18 + ], + "measured_rgb": "#DFDF6F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 86.41, + -15.7, + 60.31 + ], + "measured_rgb": "#DFDE61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 86.06, + -14.91, + 59.87 + ], + "measured_rgb": "#DFDD61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 85.89, + -14.7, + 63.29 + ], + "measured_rgb": "#E0DC58", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 86.91, + -13.69, + 66.56 + ], + "measured_rgb": "#E6DE53", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 85.49, + -13.71, + 65.56 + ], + "measured_rgb": "#E1DA52", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 86.77, + -12.65, + 71.25 + ], + "measured_rgb": "#E9DD47", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 85.39, + -13.63, + 72.72 + ], + "measured_rgb": "#E3DA3F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 85.21, + -13.5, + 76.71 + ], + "measured_rgb": "#E4D931", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 86.67, + -11.8, + 76.43 + ], + "measured_rgb": "#EBDC37", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 85.49, + -11.97, + 77.96 + ], + "measured_rgb": "#E8D92E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 85.33, + -12.02, + 80.1 + ], + "measured_rgb": "#E8D925", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 59.26, + -2.93, + -35.61 + ], + "measured_rgb": "#5593CD", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 59.41, + -2.35, + -34.23 + ], + "measured_rgb": "#5B93CB", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 53.09, + -0.39, + -40.48 + ], + "measured_rgb": "#3E83C4", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 52.81, + 0.36, + -39.81 + ], + "measured_rgb": "#4281C2", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 48.06, + 2.47, + -43.46 + ], + "measured_rgb": "#2D75BB", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 46.13, + 3.72, + -44.68 + ], + "measured_rgb": "#2670B8", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 46.04, + 3.98, + -43.55 + ], + "measured_rgb": "#2D6FB6", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 44.42, + 5.04, + -44.41 + ], + "measured_rgb": "#286BB3", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 44.98, + 5.1, + -42.4 + ], + "measured_rgb": "#336CB1", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 42.2, + 6.27, + -44.38 + ], + "measured_rgb": "#2664AD", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 39.63, + 8.04, + -46.0 + ], + "measured_rgb": "#1C5EA9", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 38.59, + 8.36, + -46.18 + ], + "measured_rgb": "#175BA6", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 37.5, + 9.22, + -46.13 + ], + "measured_rgb": "#1858A3", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 32.23, + -3.53, + -0.01 + ], + "measured_rgb": "#464E4C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 32.81, + -4.24, + 0.56 + ], + "measured_rgb": "#464F4C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 34.15, + -4.49, + 3.87 + ], + "measured_rgb": "#4B524A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 37.35, + -6.99, + 11.16 + ], + "measured_rgb": "#545B46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 36.99, + -4.61, + 9.94 + ], + "measured_rgb": "#565947", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 37.25, + -1.03, + 10.47 + ], + "measured_rgb": "#5D5847", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 42.82, + -4.42, + 20.44 + ], + "measured_rgb": "#6A6643", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 44.71, + -2.87, + 24.85 + ], + "measured_rgb": "#736A40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 46.34, + -1.41, + 27.3 + ], + "measured_rgb": "#7B6D40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 30.83, + -1.07, + -2.8 + ], + "measured_rgb": "#45494D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 32.93, + -0.38, + 3.48 + ], + "measured_rgb": "#4F4D48", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 34.58, + -2.45, + 6.26 + ], + "measured_rgb": "#525247", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 36.48, + -2.69, + 10.74 + ], + "measured_rgb": "#585745", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 36.81, + 0.18, + 11.24 + ], + "measured_rgb": "#5E5645", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 40.56, + -1.97, + 18.21 + ], + "measured_rgb": "#676042", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 44.1, + -1.72, + 24.33 + ], + "measured_rgb": "#736840", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 45.28, + 1.15, + 26.65 + ], + "measured_rgb": "#7C693E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 31.65, + 0.49, + 0.75 + ], + "measured_rgb": "#4C4A49", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 34.96, + -1.74, + 8.49 + ], + "measured_rgb": "#555345", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 35.0, + -0.46, + 8.13 + ], + "measured_rgb": "#575245", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 35.49, + 2.18, + 9.86 + ], + "measured_rgb": "#5D5244", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 37.72, + 0.55, + 13.56 + ], + "measured_rgb": "#625843", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 43.18, + 0.87, + 23.69 + ], + "measured_rgb": "#75643F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 43.6, + 2.7, + 23.97 + ], + "measured_rgb": "#78643F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 33.04, + 2.13, + 3.9 + ], + "measured_rgb": "#544C48", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 33.94, + 2.25, + 7.42 + ], + "measured_rgb": "#584E44", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 33.08, + 7.54, + 8.06 + ], + "measured_rgb": "#5E4941", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 39.5, + -1.02, + 16.66 + ], + "measured_rgb": "#655D42", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 40.42, + 3.78, + 19.13 + ], + "measured_rgb": "#705C40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 42.47, + 7.35, + 22.43 + ], + "measured_rgb": "#7C5F40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 32.03, + 5.65, + 4.57 + ], + "measured_rgb": "#574844", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 32.62, + 7.5, + 6.94 + ], + "measured_rgb": "#5C4842", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 33.68, + 8.82, + 9.29 + ], + "measured_rgb": "#624A41", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 38.68, + 6.76, + 16.51 + ], + "measured_rgb": "#6F5641", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 38.56, + 10.09, + 17.18 + ], + "measured_rgb": "#73543F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 33.4, + 5.95, + 7.54 + ], + "measured_rgb": "#5C4B43", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 33.27, + 11.92, + 8.78 + ], + "measured_rgb": "#654741", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 39.06, + 7.04, + 17.32 + ], + "measured_rgb": "#705740", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 38.92, + 11.63, + 18.79 + ], + "measured_rgb": "#77543E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 32.56, + 13.56, + 9.0 + ], + "measured_rgb": "#66443F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 33.92, + 11.71, + 10.22 + ], + "measured_rgb": "#674940", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 35.68, + 13.35, + 13.16 + ], + "measured_rgb": "#6F4C40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 32.37, + 16.51, + 9.16 + ], + "measured_rgb": "#69423E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 38.21, + 13.95, + 17.68 + ], + "measured_rgb": "#78513E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 35.06, + 16.29, + 13.06 + ], + "measured_rgb": "#71483E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 60.59, + 34.71, + 26.62 + ], + "measured_rgb": "#D67865", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 59.65, + 36.54, + 32.53 + ], + "measured_rgb": "#D87458", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 60.45, + 34.97, + 33.46 + ], + "measured_rgb": "#D87759", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 60.48, + 35.03, + 35.45 + ], + "measured_rgb": "#D97755", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 60.74, + 35.59, + 37.63 + ], + "measured_rgb": "#DB7752", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 60.84, + 33.42, + 34.85 + ], + "measured_rgb": "#D87A57", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 59.98, + 34.25, + 42.34 + ], + "measured_rgb": "#D87647", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 59.5, + 34.06, + 45.9 + ], + "measured_rgb": "#D7753F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 60.73, + 32.52, + 47.98 + ], + "measured_rgb": "#D97A3E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 57.33, + 40.74, + 32.02 + ], + "measured_rgb": "#D66A54", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 57.86, + 38.74, + 32.01 + ], + "measured_rgb": "#D56D55", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 59.26, + 36.69, + 29.23 + ], + "measured_rgb": "#D6735D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 57.69, + 38.61, + 37.53 + ], + "measured_rgb": "#D66D4B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 58.84, + 37.53, + 35.54 + ], + "measured_rgb": "#D77151", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 58.26, + 36.31, + 36.61 + ], + "measured_rgb": "#D4704E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 58.63, + 36.73, + 43.16 + ], + "measured_rgb": "#D77142", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 58.5, + 37.1, + 43.64 + ], + "measured_rgb": "#D87041", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 56.37, + 41.44, + 30.12 + ], + "measured_rgb": "#D46755", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 55.61, + 41.45, + 34.19 + ], + "measured_rgb": "#D2654C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 55.53, + 41.59, + 37.12 + ], + "measured_rgb": "#D36447", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 56.91, + 39.02, + 32.21 + ], + "measured_rgb": "#D36A53", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 57.16, + 38.42, + 33.48 + ], + "measured_rgb": "#D36C51", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 56.26, + 39.43, + 40.55 + ], + "measured_rgb": "#D36842", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 56.61, + 38.72, + 43.85 + ], + "measured_rgb": "#D4693C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 54.0, + 44.02, + 32.55 + ], + "measured_rgb": "#D05E4B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 53.03, + 44.01, + 34.64 + ], + "measured_rgb": "#CE5B45", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.36, + 44.2, + 37.81 + ], + "measured_rgb": "#D05C41", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 55.03, + 40.44, + 32.66 + ], + "measured_rgb": "#CF644D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 54.828, + 42.215, + 34.274 + ], + "measured_rgb": "#D1624A", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 55.474, + 41.194, + 34.226 + ], + "measured_rgb": "#D2654C", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 52.99, + 45.97, + 30.69 + ], + "measured_rgb": "#CF594C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 54.026, + 44.398, + 31.429 + ], + "measured_rgb": "#D15E4D", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 53.48, + 44.5, + 33.71 + ], + "measured_rgb": "#D05C48", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 53.875, + 43.798, + 34.292 + ], + "measured_rgb": "#D05E48", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 53.05, + 44.39, + 35.65 + ], + "measured_rgb": "#CF5B44", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 52.053, + 47.001, + 31.923 + ], + "measured_rgb": "#CE5548", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 51.938, + 46.902, + 33.508 + ], + "measured_rgb": "#CE5545", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 51.974, + 46.493, + 35.433 + ], + "measured_rgb": "#CE5642", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 52.243, + 45.967, + 35.487 + ], + "measured_rgb": "#CE5742", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 51.23, + 48.13, + 31.57 + ], + "measured_rgb": "#CD5247", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 51.072, + 48.014, + 34.379 + ], + "measured_rgb": "#CD5242", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 50.05, + 49.01, + 38.06 + ], + "measured_rgb": "#CC4D39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 50.014, + 48.998, + 33.907 + ], + "measured_rgb": "#CB4E40", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 50.106, + 48.917, + 34.856 + ], + "measured_rgb": "#CB4E3F", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 48.46, + 50.2, + 35.77 + ], + "measured_rgb": "#C84839", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 43.95, + 12.77, + -5.1 + ], + "measured_rgb": "#796171", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 42.04, + 9.64, + -8.16 + ], + "measured_rgb": "#6D5E71", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 41.99, + 8.57, + -9.11 + ], + "measured_rgb": "#6B5F72", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 37.82, + 7.98, + -12.1 + ], + "measured_rgb": "#5D566D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 37.04, + 10.17, + -6.18 + ], + "measured_rgb": "#635261", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 35.88, + 6.85, + -11.4 + ], + "measured_rgb": "#575267", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 34.82, + 8.38, + -8.59 + ], + "measured_rgb": "#594E60", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 33.2, + 9.06, + -8.11 + ], + "measured_rgb": "#574A5B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 32.07, + 8.43, + -9.28 + ], + "measured_rgb": "#52485A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 43.13, + 13.68, + -2.83 + ], + "measured_rgb": "#7A5E6B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 40.14, + 12.29, + -4.61 + ], + "measured_rgb": "#6F5866", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 37.63, + 10.55, + -7.27 + ], + "measured_rgb": "#645364", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 38.38, + 9.16, + -8.16 + ], + "measured_rgb": "#635668", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 35.95, + 10.58, + -5.28 + ], + "measured_rgb": "#624F5D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 34.42, + 9.15, + -7.21 + ], + "measured_rgb": "#5A4C5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 34.05, + 8.82, + -7.67 + ], + "measured_rgb": "#594C5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 31.57, + 8.0, + -9.3 + ], + "measured_rgb": "#504759", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 42.67, + 15.13, + -1.45 + ], + "measured_rgb": "#7C5C68", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 38.88, + 13.65, + -2.95 + ], + "measured_rgb": "#6F5461", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 36.38, + 12.23, + -5.02 + ], + "measured_rgb": "#664F5E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 36.56, + 10.6, + -6.32 + ], + "measured_rgb": "#635160", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 33.78, + 10.4, + -5.72 + ], + "measured_rgb": "#5C4A59", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 33.68, + 9.66, + -5.71 + ], + "measured_rgb": "#5B4A58", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 31.46, + 8.64, + -7.9 + ], + "measured_rgb": "#524656", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 37.64, + 16.45, + -0.45 + ], + "measured_rgb": "#724F5A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 35.97, + 14.23, + -2.54 + ], + "measured_rgb": "#694D59", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 33.12, + 14.21, + -3.07 + ], + "measured_rgb": "#624653", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 34.92, + 11.2, + -4.87 + ], + "measured_rgb": "#614C5A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 32.77, + 11.87, + -3.19 + ], + "measured_rgb": "#5D4752", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 30.53, + 13.08, + -1.32 + ], + "measured_rgb": "#5B414A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 39.18, + 16.47, + 0.67 + ], + "measured_rgb": "#76535C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 33.52, + 16.44, + 0.11 + ], + "measured_rgb": "#68454F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 33.8, + 14.01, + -1.98 + ], + "measured_rgb": "#644853", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 32.67, + 16.81, + 2.7 + ], + "measured_rgb": "#674349", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 31.2, + 13.76, + -0.59 + ], + "measured_rgb": "#5E424B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 37.08, + 19.55, + 3.74 + ], + "measured_rgb": "#774B52", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 32.33, + 16.27, + -0.27 + ], + "measured_rgb": "#64434D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 32.15, + 14.65, + -1.47 + ], + "measured_rgb": "#61434E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 31.51, + 13.89, + -1.21 + ], + "measured_rgb": "#5E424C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 36.64, + 20.21, + 4.79 + ], + "measured_rgb": "#774A4F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 33.48, + 17.53, + 2.16 + ], + "measured_rgb": "#6A444C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 32.33, + 15.16, + 0.23 + ], + "measured_rgb": "#63434C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 34.44, + 20.68, + 5.02 + ], + "measured_rgb": "#72444A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 31.82, + 18.04, + 1.96 + ], + "measured_rgb": "#674048", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 33.8, + 20.81, + 5.55 + ], + "measured_rgb": "#714248", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 61.95, + -24.93, + 11.39 + ], + "measured_rgb": "#6CA181", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 58.64, + -24.28, + 6.71 + ], + "measured_rgb": "#609881", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 53.18, + -26.38, + 5.24 + ], + "measured_rgb": "#4A8B75", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 48.85, + -26.53, + 1.34 + ], + "measured_rgb": "#388071", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 47.76, + -25.67, + 0.94 + ], + "measured_rgb": "#377D6F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 47.71, + -23.33, + -0.88 + ], + "measured_rgb": "#3B7C72", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 45.3, + -23.42, + -3.16 + ], + "measured_rgb": "#307670", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 42.32, + -22.68, + -5.37 + ], + "measured_rgb": "#256E6C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 40.66, + -22.75, + -6.54 + ], + "measured_rgb": "#1C6A6A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 58.19, + -28.22, + 13.86 + ], + "measured_rgb": "#5C9973", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 57.54, + -26.75, + 11.39 + ], + "measured_rgb": "#5C9675", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 54.04, + -25.84, + 7.96 + ], + "measured_rgb": "#518D73", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 49.15, + -27.67, + 5.27 + ], + "measured_rgb": "#3B816B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 50.78, + -24.99, + 5.57 + ], + "measured_rgb": "#48846F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 46.19, + -27.43, + 6.2 + ], + "measured_rgb": "#367962", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 44.25, + -26.25, + 2.19 + ], + "measured_rgb": "#2D7464", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 43.05, + -25.29, + -0.35 + ], + "measured_rgb": "#297166", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 60.51, + -27.72, + 13.78 + ], + "measured_rgb": "#649F79", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 56.39, + -27.38, + 13.6 + ], + "measured_rgb": "#5A936F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 55.04, + -26.57, + 11.27 + ], + "measured_rgb": "#56906F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 50.22, + -27.61, + 7.99 + ], + "measured_rgb": "#428469", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 48.43, + -28.67, + 10.95 + ], + "measured_rgb": "#3F7F60", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 46.49, + -27.7, + 8.65 + ], + "measured_rgb": "#397A5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 45.03, + -26.36, + 4.91 + ], + "measured_rgb": "#347662", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 60.7, + -28.15, + 21.36 + ], + "measured_rgb": "#6A9F6C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 58.01, + -27.55, + 16.4 + ], + "measured_rgb": "#60986E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.3, + -28.62, + 14.7 + ], + "measured_rgb": "#508C65", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 51.14, + -28.08, + 12.6 + ], + "measured_rgb": "#498663", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 48.27, + -28.82, + 13.08 + ], + "measured_rgb": "#407F5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 44.97, + -27.91, + 6.3 + ], + "measured_rgb": "#31765F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 59.26, + -29.58, + 23.49 + ], + "measured_rgb": "#659C64", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 55.03, + -29.9, + 20.19 + ], + "measured_rgb": "#569160", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 52.18, + -28.85, + 16.83 + ], + "measured_rgb": "#4F895F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 51.03, + -30.5, + 17.18 + ], + "measured_rgb": "#48865B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 50.23, + -27.11, + 13.19 + ], + "measured_rgb": "#4A8360", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 59.6, + -28.48, + 25.96 + ], + "measured_rgb": "#6A9C61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 56.1, + -28.9, + 21.55 + ], + "measured_rgb": "#5D9360", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 52.36, + -29.7, + 18.82 + ], + "measured_rgb": "#4F8A5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 50.32, + -30.46, + 18.03 + ], + "measured_rgb": "#478558", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 58.9, + -29.55, + 28.26 + ], + "measured_rgb": "#689A5B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 55.52, + -29.56, + 25.37 + ], + "measured_rgb": "#5D9258", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 51.61, + -30.92, + 22.57 + ], + "measured_rgb": "#4D8853", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 57.98, + -30.58, + 31.16 + ], + "measured_rgb": "#659853", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 55.14, + -30.46, + 27.15 + ], + "measured_rgb": "#5B9153", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 57.58, + -31.03, + 33.45 + ], + "measured_rgb": "#65974D", + "source": "measured" + } + ] +} diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml index 94c98121ec..d081e2f997 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml @@ -276,6 +276,12 @@ modules: sha256: 27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77 dest: external-packages/Draco + # Assimp 5.4.3 + - type: file + url: https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz + sha256: 66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb + dest: external-packages/Assimp + # OpenSSL 1.1.1w (GNOME SDK has 3.x; OrcaSlicer requires 1.1.x) - type: file url: https://github.com/openssl/openssl/archive/OpenSSL_1_1_1w.tar.gz diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 1e01395eb9..b5b159a31a 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -179,6 +179,17 @@ set(lisbslic3r_sources Fill/Lightning/Layer.hpp Fill/Lightning/TreeNode.cpp Fill/Lightning/TreeNode.hpp + FilamentMixer.cpp + FilamentMixer.hpp + FilamentMixerModel.hpp + ColorDecomposeRecipe.cpp + ColorDecomposeRecipe.hpp + TexturePainting.hpp + TexturePainting.cpp + TextureToColor/TextureToColor.hpp + TextureToColor/TextureToColor.cpp + TextureToColor/ColorUtils.hpp + TextureToColor/ColorUtils.cpp Flow.cpp Flow.hpp FlushVolCalc.cpp @@ -194,6 +205,9 @@ set(lisbslic3r_sources format.hpp Format/OBJ.cpp Format/OBJ.hpp + Format/AssimpImport.hpp + Format/AssimpImport.cpp + Format/ResourcePathUtils.hpp Format/objparser.cpp Format/objparser.hpp Format/SL1.cpp @@ -511,6 +525,7 @@ cmake_policy(SET CMP0011 NEW) set(CMAKE_POLICY_DEFAULT_CMP0167 NEW) find_package(CGAL REQUIRED) find_package(OpenCV REQUIRED core) +find_package(assimp REQUIRED) unset(CMAKE_POLICY_DEFAULT_CMP0167) cmake_policy(POP) @@ -551,7 +566,7 @@ target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTI if (USE_SLIC3R_CONSOLE_LOG) target_compile_definitions(libslic3r PRIVATE $<$:SLIC3R_CONSOLE_LOG>) endif() -target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/TextureToColor PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS}) # Find the OCCT and related libraries @@ -599,6 +614,7 @@ target_link_libraries(libslic3r libnest2d miniz opencv_world + assimp::assimp PRIVATE ${CMAKE_DL_LIBS} ${EXPAT_LIBRARIES} diff --git a/src/libslic3r/ColorDecomposeRecipe.cpp b/src/libslic3r/ColorDecomposeRecipe.cpp new file mode 100644 index 0000000000..f2ceb1860a --- /dev/null +++ b/src/libslic3r/ColorDecomposeRecipe.cpp @@ -0,0 +1,530 @@ +#include "ColorDecomposeRecipe.hpp" + +#include "FilamentMixer.hpp" +#include "Utils.hpp" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include +#include + +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 component_keys; + std::vector component_hexes; + std::vector 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(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> ratio_grid(size_t n) +{ + std::vector> 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 load_standard_entries() +{ + std::vector 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()); + } + } + if (item.contains("measured_lab") && item["measured_lab"].is_array() && item["measured_lab"].size() >= 3) { + entry.measured_lab = { + item["measured_lab"][0].get(), + item["measured_lab"][1].get(), + item["measured_lab"][2].get() + }; + } 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& standard_entries() +{ + static const std::vector entries = load_standard_entries(); + return entries; +} + +static void evaluate_candidate(const ColorDecomposeRgb& target, + const std::vector& hexes, + const std::vector& ratios, + const std::vector& 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(r), static_cast(g), static_cast(b)}; + return true; +} + +ColorDecomposeRecipeResult recommend_from_physical_filaments( + const ColorDecomposeRgb& target, + const std::vector& physical_filaments, + const std::string& preferred_material_type) +{ + std::vector 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::max(); + + for (size_t i = 0; i < candidates.size(); ++i) { + for (size_t j = i + 1; j < candidates.size(); ++j) { + const std::vector hexes = {candidates[i].color_hex, candidates[j].color_hex}; + const std::vector 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 hexes3 = {candidates[i].color_hex, candidates[j].color_hex, candidates[k].color_hex}; + const std::vector 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::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& component_hexes, + const std::vector& 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> 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 in_hexes; + std::vector 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(std::lround( + static_cast(in_ratios[i]) * 100.0 / static_cast(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 ratios; + LabColor lab; + std::string hex; + }; + std::vector anchors; + + for (const StandardRecipeEntry& entry : standard_entries()) { + if (entry.source != "measured" && entry.source != "interpolated") + continue; + if (entry.component_hexes.size() != n) + continue; + + std::vector> 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(in_ratios[0]); + size_t lo = 0; + while (lo + 2 < anchors.size() && static_cast(anchors[lo + 1].ratios[0]) <= x) + ++lo; + const Anchor& a0 = anchors[lo]; + const Anchor& a1 = anchors[lo + 1]; + const double span = static_cast(a1.ratios[0] - a0.ratios[0]); + const double t = span > 0.0 ? (x - static_cast(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(in_ratios[0]); + const double rb = static_cast(in_ratios[1]); + std::vector> dists; + dists.reserve(anchors.size()); + for (const Anchor& a : anchors) { + const double d = std::sqrt(std::pow(ra - static_cast(a.ratios[0]), 2.0) + + std::pow(rb - static_cast(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(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 diff --git a/src/libslic3r/ColorDecomposeRecipe.hpp b/src/libslic3r/ColorDecomposeRecipe.hpp new file mode 100644 index 0000000000..146bf322a2 --- /dev/null +++ b/src/libslic3r/ColorDecomposeRecipe.hpp @@ -0,0 +1,64 @@ +#ifndef SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP +#define SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP + +#include +#include + +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 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& 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& component_hexes, + const std::vector& ratios); + +} // namespace Slic3r + +#endif // SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP diff --git a/src/libslic3r/FilamentMixer.cpp b/src/libslic3r/FilamentMixer.cpp new file mode 100644 index 0000000000..66640498e6 --- /dev/null +++ b/src/libslic3r/FilamentMixer.cpp @@ -0,0 +1,829 @@ +#include "FilamentMixer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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(clamped * 255.0f + 0.5f); +} + +inline float to_f01(unsigned char x) +{ + return static_cast(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 &hex_colors, + const std::vector &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(weights[i]) / static_cast(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 parse_mixed_components(const std::string &str) +{ + std::vector 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(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::quiet_NaN(); + std::string lower(tok.size(), '\0'); + std::transform(tok.begin(), tok.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lower == "nan") return std::numeric_limits::quiet_NaN(); + try { + const double v = std::stod(tok); + if (!std::isfinite(v)) return std::numeric_limits::quiet_NaN(); + return v; + } catch (...) { + return std::numeric_limits::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 split_commas(const std::string& seg) +{ + std::vector 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 compute_pchip_default_tangents(const std::vector& pts) +{ + const size_t n = pts.size(); + std::vector m(n, 0.0); + if (n < 2) return m; + + std::vector 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 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 parse_mixed_ratios(const std::string &str, size_t n_components) +{ + CNumericLocalesSetter c_locale_setter; + std::vector 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 &is_mixed) +{ + for (unsigned char v : is_mixed) + if (v) return true; + return false; +} + +std::vector check_mixed_filament_integrity( + const std::vector &is_mixed, + const std::vector &comp_strs, + size_t num_physical) +{ + std::vector 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 expand_mixed_filaments( + const std::vector &extruders_0based, + const std::vector &is_mixed, + const std::vector &comp_strs) +{ + std::vector 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 &is_mixed, + std::vector &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 check_mixed_filament_type_consistency( + const std::vector &is_mixed, + const std::vector &comp_strs, + const std::vector &filament_types) +{ + std::vector 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(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> &unprintables, + const std::vector &is_mixed, + const std::vector &comp_strs) +{ + for (auto &unprintable_set : unprintables) { + std::set 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& 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 &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(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 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 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 &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 &comp_strs, + const std::vector &ratio_strs, + const std::vector &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 &vals) +{ + for (const std::string &s : vals) + if (!s.empty()) + return true; + return false; +} + +static bool mixed_bool_array_was_specified(const std::vector &vals) +{ + for (unsigned char v : vals) + if (v) + return true; + return false; +} + +static void check_mixed_array_size_required(std::map &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 validate_mixed_filament_params( + const std::vector &is_mixed, + const std::vector &comp_strs, + const std::vector &ratio_strs, + const std::vector &gradient_flags, + const std::vector &gradient_range_strs, + const std::vector &gradient_curve_strs) +{ + std::map 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 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(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 diff --git a/src/libslic3r/FilamentMixer.hpp b/src/libslic3r/FilamentMixer.hpp new file mode 100644 index 0000000000..81ddd29e46 --- /dev/null +++ b/src/libslic3r/FilamentMixer.hpp @@ -0,0 +1,164 @@ +#ifndef SLIC3R_FILAMENT_MIXER_HPP +#define SLIC3R_FILAMENT_MIXER_HPP + +#include +#include +#include +#include +#include +#include + +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::quiet_NaN(); + double m_out = std::numeric_limits::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 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 compute_pchip_default_tangents(const std::vector& 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 &hex_colors, + const std::vector &weights); + +// Parse comma-separated 1-based component IDs, e.g. "1,3" → {1, 3}. +std::vector 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 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. +bool has_any_mixed_filament(const std::vector &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 check_mixed_filament_integrity( + const std::vector &is_mixed, + const std::vector &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 expand_mixed_filaments( + const std::vector &extruders_0based, + const std::vector &is_mixed, + const std::vector &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 &is_mixed, + std::vector &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 check_mixed_filament_type_consistency( + const std::vector &is_mixed, + const std::vector &comp_strs, + const std::vector &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> &unprintables, + const std::vector &is_mixed, + const std::vector &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& 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 validate_mixed_filament_params( + const std::vector &is_mixed, + const std::vector &comp_strs, + const std::vector &ratio_strs, + const std::vector &gradient_flags, + const std::vector &gradient_range_strs, + const std::vector &gradient_curve_strs); + +} // namespace Slic3r + +#endif // SLIC3R_FILAMENT_MIXER_HPP diff --git a/src/libslic3r/FilamentMixerModel.hpp b/src/libslic3r/FilamentMixerModel.hpp new file mode 100644 index 0000000000..89b299471b --- /dev/null +++ b/src/libslic3r/FilamentMixerModel.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 +#include +#include + +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(r1), static_cast(g1), static_cast(b1), + static_cast(r2), static_cast(g2), static_cast(b2), + static_cast(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(sum); + if (val < 0) val = 0; + if (val > 255) val = 255; + + if (c == 0) *out_r = static_cast(val); + else if (c == 1) *out_g = static_cast(val); + else *out_b = static_cast(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 diff --git a/src/libslic3r/Fill/FillCornerSmoothing.cpp b/src/libslic3r/Fill/FillCornerSmoothing.cpp index 2af9f6bb9c..dbce39d572 100644 --- a/src/libslic3r/Fill/FillCornerSmoothing.cpp +++ b/src/libslic3r/Fill/FillCornerSmoothing.cpp @@ -108,6 +108,22 @@ const std::vector& 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(); diff --git a/src/libslic3r/Fill/FillCornerSmoothing.hpp b/src/libslic3r/Fill/FillCornerSmoothing.hpp index 1852fc4c67..7f2ead229a 100644 --- a/src/libslic3r/Fill/FillCornerSmoothing.hpp +++ b/src/libslic3r/Fill/FillCornerSmoothing.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -47,36 +48,57 @@ public: template 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 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 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& 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 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. diff --git a/src/libslic3r/Format/AssimpImport.cpp b/src/libslic3r/Format/AssimpImport.cpp new file mode 100644 index 0000000000..f0ae99506a --- /dev/null +++ b/src/libslic3r/Format/AssimpImport.cpp @@ -0,0 +1,327 @@ +#include "AssimpImport.hpp" + +#include "../TexturePainting.hpp" +#include "ResourcePathUtils.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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(size) > static_cast(std::numeric_limits::max())) + return false; + + file.seekg(0); + out.width = -1; + out.height = -1; + out.channels = 0; + out.data.resize(static_cast(size)); + file.read(reinterpret_cast(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(texture.pcData), + reinterpret_cast(texture.pcData) + texture.mWidth); + return !out.data.empty(); + } + + if (texture.mWidth == 0 || texture.mHeight == 0) + return false; + if (texture.mWidth > static_cast(std::numeric_limits::max()) || + texture.mHeight > static_cast(std::numeric_limits::max())) { + return false; + } + const size_t width = static_cast(texture.mWidth); + const size_t height = static_cast(texture.mHeight); + if (width > std::numeric_limits::max() / height || + width * height > std::numeric_limits::max() / 4) { + return false; + } + + out.width = static_cast(texture.mWidth); + out.height = static_cast(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 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(std::numeric_limits::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(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(static_cast(face.mIndices[0]) + vertex_offset), + static_cast(static_cast(face.mIndices[1]) + vertex_offset), + static_cast(static_cast(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(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 diff --git a/src/libslic3r/Format/AssimpImport.hpp b/src/libslic3r/Format/AssimpImport.hpp new file mode 100644 index 0000000000..80c3e2dc91 --- /dev/null +++ b/src/libslic3r/Format/AssimpImport.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace Slic3r { + +struct TexturedMesh; + +bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr); + +} // namespace Slic3r diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index 71f7d1e7e2..a91cecd8dc 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -1,6 +1,8 @@ #include "../libslic3r.h" #include "../Model.hpp" #include "../TriangleMesh.hpp" +#include "../TexturePainting.hpp" +#include "ResourcePathUtils.hpp" #include "OBJ.hpp" #include "objparser.hpp" @@ -8,6 +10,7 @@ #include #include +#include #ifdef _WIN32 #define DIR_SEPARATOR '\\' @@ -21,7 +24,7 @@ namespace Slic3r { -bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message) +bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl) { if (meshptr == nullptr) return false; @@ -98,6 +101,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s its.indices.reserve(num_faces + num_quads); if (exist_mtl) { obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1; + obj_info.usemtls = data.usemtls; obj_info.face_colors.reserve(num_faces + num_quads); } bool has_color = data.has_vertex_color; @@ -210,14 +214,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s } if (meshptr->volume() < 0) meshptr->flip_triangles(); + // Hand the parsed material table back so callers can build a TexturedMesh from it. + if (out_mtl) + *out_mtl = mtl_data; return true; } -bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in) +bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl) { TriangleMesh mesh; - bool ret = load_obj(path, &mesh, obj_info, message); + bool ret = load_obj(path, &mesh, obj_info, message, out_mtl); if (ret) { std::string object_name; @@ -232,6 +239,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me return ret; } +bool obj_to_textured_mesh( + const ObjInfo& obj_info, + const indexed_triangle_set& its, + const ObjParser::MtlData& mtl_data, + const std::string& obj_directory, + TexturedMesh& out) +{ + if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png) + return false; + + const size_t nv = its.vertices.size(); + const size_t nf = its.indices.size(); + + // 1. Copy vertices + out.vertices.resize(nv); + for (size_t i = 0; i < nv; ++i) + out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()}; + + // 2. Copy face indices + out.indices.resize(nf); + for (size_t i = 0; i < nf; ++i) + out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]}; + + // 3. Build per-face UV (uv_coords + uv_indices) + // OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV). + // Flip V here so downstream code works uniformly. + if (!obj_info.uvs.empty()) { + const size_t uv_face_count = obj_info.uvs.size(); + out.uv_coords.resize(uv_face_count * 3); + out.uv_indices.resize(nf); + for (size_t fi = 0; fi < nf; ++fi) { + if (fi < uv_face_count) { + int base = static_cast(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 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(i); + + const int num_materials = static_cast(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 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(file_size)); + file.read(reinterpret_cast(ti.data.data()), file_size); + ti.width = -1; + ti.height = -1; + ti.channels = 0; + + int new_idx = static_cast(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(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. diff --git a/src/libslic3r/Format/OBJ.hpp b/src/libslic3r/Format/OBJ.hpp index 2d4370c99a..c103326af6 100644 --- a/src/libslic3r/Format/OBJ.hpp +++ b/src/libslic3r/Format/OBJ.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_Format_OBJ_hpp_ #define slic3r_Format_OBJ_hpp_ #include "libslic3r/Color.hpp" +#include "objparser.hpp" #include namespace Slic3r { @@ -18,6 +19,7 @@ struct ObjInfo { std::map pngs; std::unordered_map uv_map_pngs; bool has_uv_png{false}; + std::vector usemtls; // material spans, for texture import }; struct ObjDialogInOut @@ -32,8 +34,18 @@ struct ObjDialogInOut std::string lost_material_name{""}; }; typedef std::function 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); diff --git a/src/libslic3r/Format/ResourcePathUtils.hpp b/src/libslic3r/Format/ResourcePathUtils.hpp new file mode 100644 index 0000000000..d82b92bd45 --- /dev/null +++ b/src/libslic3r/Format/ResourcePathUtils.hpp @@ -0,0 +1,240 @@ +#ifndef slic3r_Format_ResourcePathUtils_hpp_ +#define slic3r_Format_ResourcePathUtils_hpp_ + +#include +#include +#include +#include +#include + +#include +#include + +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(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((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(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(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 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_ */ diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 15f3f3c0f0..1d901dad6f 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -4,6 +4,7 @@ #include "../Preset.hpp" #include "../Utils.hpp" #include "../LocalesUtils.hpp" +#include "../FilamentMixer.hpp" #include "../GCode.hpp" #include "../Geometry.hpp" #include "../GCode/ThumbnailData.hpp" @@ -246,6 +247,8 @@ static constexpr const char* BUILD_TAG = "build"; static constexpr const char* ITEM_TAG = "item"; static constexpr const char* METADATA_TAG = "metadata"; static constexpr const char* FILAMENT_TAG = "filament"; +static constexpr const char* MIXED_FILAMENT_TAG = "mixed_filament"; +static constexpr const char* MIXED_FILAMENT_COMPONENTS_TAG = "components"; static constexpr const char* SLICE_WARNING_TAG = "warning"; static constexpr const char* WARNING_MSG_TAG = "msg"; static constexpr const char *FILAMENT_ID_TAG = "id"; @@ -1334,6 +1337,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _handle_end_config_metadata(); bool _handle_start_config_filament(const char** attributes, unsigned int num_attributes); + bool _handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes); bool _handle_end_config_filament(); bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes); @@ -2713,6 +2717,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return; } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", load project config file successfully from %1%\n") %dest_file; + + // Heal any gradient-curve slots corrupted by the legacy "|" separator collision + // (see FilamentMixer::sanitize_mixed_gradient_curve_array). The 3MF JSON itself + // is safe (";" + C-style escape), but older projects saved through the buggy + // export_selections/load_selections path may already carry single-point entries + // that fail MakerWorld's "curve needs >= 2 points" check. + if (auto* curve_opt = config.option("filament_mixed_gradient_curve")) + Slic3r::sanitize_mixed_gradient_curve_array(curve_opt->values); } } @@ -3530,6 +3542,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_start_config_plater_instance(attributes, num_attributes); else if (::strcmp(FILAMENT_TAG, name) == 0) res = _handle_start_config_filament(attributes, num_attributes); + else if (::strcmp(MIXED_FILAMENT_TAG, name) == 0) + res = _handle_start_config_mixed_filament(attributes, num_attributes); else if (::strcmp(SLICE_WARNING_TAG, name) == 0) res = _handle_start_config_warning(attributes, num_attributes); else if (::strcmp(NOZZLE_TAG, name) == 0) @@ -4703,6 +4717,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_3MF_Importer::_handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes) + { + if (m_curr_plater) { + std::string id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_ID_TAG); + std::string type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TYPE_TAG); + std::string color = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_COLOR_TAG); + std::string components = bbs_get_attribute_value_string(attributes, num_attributes, MIXED_FILAMENT_COMPONENTS_TAG); + PlateMixedFilamentInfo mixed_info; + mixed_info.id = atoi(id.c_str()); + mixed_info.type = type; + mixed_info.color = color; + mixed_info.components = components; + m_curr_plater->mixed_filaments_info.push_back(mixed_info); + } + return true; + } + bool _BBS_3MF_Importer::_handle_end_config_filament() { // do nothing @@ -8520,6 +8551,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) << FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n"; } + // Mixed (virtual) filaments used by this plate. These are resolved to physical + // components before g-code statistics, so they are not present in the + // 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"; } diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 46d06199fa..2e1600737b 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -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 slice_filaments_info; + // Mixed (virtual) filaments used by this plate; empty when no mixed filament is used. + std::vector mixed_filaments_info; std::vector skipped_objects; DynamicPrintConfig config; bool is_support_used {false}; diff --git a/src/libslic3r/Format/objparser.cpp b/src/libslic3r/Format/objparser.cpp index 82bf2b4963..886fa423bd 100644 --- a/src/libslic3r/Format/objparser.cpp +++ b/src/libslic3r/Format/objparser.cpp @@ -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(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(); + 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; } diff --git a/src/libslic3r/Format/objparser.hpp b/src/libslic3r/Format/objparser.hpp index 48493de3de..58afd015a8 100644 --- a/src/libslic3r/Format/objparser.hpp +++ b/src/libslic3r/Format/objparser.hpp @@ -122,6 +122,9 @@ struct MtlData // Version of the data structure for load / store in the private binary format. int version; std::unordered_map> 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 mtl_orders; }; extern bool objparse(const char *path, ObjData &data); extern bool mtlparse(const char *path, MtlData &data); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 18d805936e..f28918f05d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -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 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 filament_instances_id; - for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id); + std::set 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 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(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(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 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 region_filter; + bool skip = false; + }; + std::vector emit_plan; + + auto compute_sub_zh = [&](double r1, double r2, double &out_sub_h, double &out_sub_z) { + std::vector sub_heights_local(grp.components_0based.size()); + for (size_t ci = 0; ci < grp.components_0based.size(); ++ci) + sub_heights_local[ci] = (static_cast(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 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 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 &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 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(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; } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 6bdb04a8a9..990bf0fee7 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -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; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index b13273d696..4f3f95f297 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2543,6 +2543,7 @@ void GCodeProcessorResult::reset() { spiral_vase_mode = false; layer_filaments.clear(); filament_change_sequence.clear(); + used_mixed_filaments.clear(); nozzle_change_sequence.clear(); optimal_assignment.clear(); filament_change_count_map.clear(); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 505f7c06a0..0f211f133e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -306,6 +306,9 @@ class Print; std::unordered_map, std::vector>,FilamentSequenceHash> layer_filaments; std::vector nozzle_change_sequence; std::vector 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 used_mixed_filaments; std::vector optimal_assignment; // first key stores `from` filament, second keys stores the `to` filament std::map, 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; diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index f37025d4e7..0a97e7ac41 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -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 +#include #include #include +#include +#include +#include +#include #include #include @@ -84,22 +91,28 @@ bool check_filament_printable_after_group(const std::vector &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 mixed_slots_1based; + std::set gradient_slots_1based; + std::set 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(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(i + 1)); + if (i < per_part_flags.size() && per_part_flags[i]) + per_part_slots_1based.insert(static_cast(i + 1)); + } + } + // BBS: collect first layer extruders of an object's wall, which will be used by brim generator int layerCount = 0; std::vector 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(&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(&layer_tools - m_layer_tools.data()); + std::set 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(&layer_tools - m_layer_tools.data()); + std::set> 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(rcfg.outer_wall_filament_id.value), + static_cast(rcfg.inner_wall_filament_id.value), + static_cast(rcfg.sparse_infill_filament_id.value), + static_cast(rcfg.top_surface_filament_id.value), + static_cast(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 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 components; // 1-based + std::vector ratios; + std::vector accum; // deficit accumulator (integer, unit: 1e-6 mm) + }; + std::vector 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 is_gradient(is_mixed.size(), false); + std::vector 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 run_lengths; + int current_run = -1; + size_t current_idx = 0; + bool prev_appeared = false; + bool last_absent_was_relevant = false; + }; + std::map gradient_runs; + for (size_t i = 0; i < is_mixed.size(); ++i) + if (is_gradient[i]) gradient_runs[static_cast(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> 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 run_start_offsets; // index into layer_indices where each run starts + std::vector 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& obj_set, + const std::set& 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& layer_indices, + const std::set& obj_set, + const std::set& 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> 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 all_obj_set(all_obj_layers.begin(), all_obj_layers.end()); + std::set 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> 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 all_obj_set(all_obj_layers.begin(), all_obj_layers.end()); + std::set 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 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 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(< - 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 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 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(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 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 sorted_comps; + std::vector 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(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 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> adj; + std::map in_degree; + std::set> 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 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, decltype(cmp)> pq(cmp); + + for (unsigned int ext : lt.extruders) { + if (in_degree[ext] == 0) + pq.push(ext); + } + + std::vector 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 used_filaments = collect_sorted_used_filaments(layer_filaments); std::vector>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>physical_unprintables = m_print->get_physical_unprintable_filaments(used_filaments); auto filament_unprintable_volumes = m_print->get_filament_unprintable_flow(used_filaments); diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index c77b152fe9..4dc08c0e8b 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -5,12 +5,16 @@ #include "../libslic3r.h" +#include +#include #include #include #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 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 components_0based; + std::vector 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 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{}(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 per_volume_gradient; + }; + std::vector 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& 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& 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 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 m_all_printing_extruders; + std::vector 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>> 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> 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>> m_gradient_volume_layers; const PrintObject* m_print_object_ptr = nullptr; Print* m_print; bool m_sorted = false; diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index b3a145bed0..87ad11bcf8 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -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(); diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index d321072335..308752cc62 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -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; diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index c689c7ce78..600c46e7f5 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1,6 +1,8 @@ #include "Model.hpp" #include "libslic3r.h" #include "BuildVolume.hpp" +#include "TexturePainting.hpp" +#include "Format/AssimpImport.hpp" #include "ClipperUtils.hpp" #include "Exception.hpp" #include "Model.hpp" @@ -104,6 +106,7 @@ Model& Model::assign_copy(const Model &rhs) this->mk_version = rhs.mk_version; this->md_name = rhs.md_name; this->md_value = rhs.md_value; + this->texture_mesh = rhs.texture_mesh; return *this; } @@ -139,6 +142,7 @@ Model& Model::assign_copy(Model &&rhs) this->mk_version = rhs.mk_version; this->md_name = rhs.md_name; this->md_value = rhs.md_value; + this->texture_mesh = std::move(rhs.texture_mesh); this->backup_path = std::move(rhs.backup_path); this->object_backup_id_map = std::move(rhs.object_backup_id_map); this->next_object_backup_id = rhs.next_object_backup_id; @@ -239,6 +243,27 @@ _finished: // BBS: add part plate related logic // BBS: backup & restore // Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +// Build a plain geometry ModelObject from a textured mesh. The texture itself is carried +// separately on Model::texture_mesh and consumed by the texture import dialog. +static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mesh, const std::string& input_file) +{ + std::string object_name = boost::filesystem::path(input_file).filename().string(); + + indexed_triangle_set its; + its.vertices.resize(tex_mesh.vertices.size()); + for (size_t i = 0; i < tex_mesh.vertices.size(); ++i) + its.vertices[i] = Vec3f(tex_mesh.vertices[i][0], tex_mesh.vertices[i][1], tex_mesh.vertices[i][2]); + its.indices.resize(tex_mesh.indices.size()); + for (size_t i = 0; i < tex_mesh.indices.size(); ++i) + its.indices[i] = Vec3i32(tex_mesh.indices[i][0], tex_mesh.indices[i][1], tex_mesh.indices[i][2]); + + its_merge_vertices(its); + its_remove_degenerate_faces(its); + its_compactify_vertices(its); + + model.add_object(object_name.c_str(), input_file.c_str(), std::move(TriangleMesh(std::move(its)))); +} + Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, @@ -281,32 +306,85 @@ Model Model::read_from_file(const std::string& result = load_stl(input_file.c_str(), &model, nullptr, stlFn,256); else if (boost::algorithm::iends_with(input_file, ".obj")) { ObjInfo obj_info; - result = load_obj(input_file.c_str(), &model, obj_info, message); - if (result){ - ObjDialogInOut in_out; - in_out.model = &model; - in_out.lost_material_name = obj_info.lost_material_name; + ObjParser::MtlData mtl_data; + result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data); + if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) { + // Textured OBJ: hand the mesh + materials to the texture-to-color importer + // instead of the flat per-face colour dialog. + auto tex_mesh = std::make_shared(); + 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(); + 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(); + result = load_assimp_textured_model(input_file, *tex_mesh, &message); + if (result) { + model.texture_mesh = tex_mesh; + add_textured_mesh_to_model(model, *tex_mesh, input_file); + } else if (!message.empty()) { + BOOST_LOG_TRIVIAL(error) << "Assimp: failed to load model: " << message + << ", path=" << input_file; + message = _L("The file format is incompatible and cannot be parsed."); } } else if (boost::algorithm::iends_with(input_file, ".svg")) @@ -578,6 +656,7 @@ void Model::clear_objects() this->objects.clear(); object_backup_id_map.clear(); next_object_backup_id = 1; + texture_mesh.reset(); } // BBS: backup, reuse objects @@ -2576,7 +2655,8 @@ void ModelVolume::update_extruder_count(size_t extruder_count) } } -void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id) +void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id, + const std::vector &filament_is_mixed) { std::vector used_extruders = get_extruders(); for (int extruder_id : used_extruders) { @@ -2587,8 +2667,22 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou } // Same stale-assignment cleanup as update_extruder_count, for the filament-delete path. // Ported from BambuStudio (STUDIO-15763). - if (extruder_id() > extruder_count) { - this->config.erase("extruder"); + size_t eid = extruder_id(); + // Judge out-of-range against the post-remap id, mirroring update_filament_values_for_items_when_delete_filament. + // Using the pre-remap eid would wrongly erase a high extruder that should remap (e.g. 5 -> 4 after + // deleting filament 1); update_filament_values_for_items_when_delete_filament would then skip it + // (!has("extruder")) and the volume would fall back to the object default color. + size_t remapped = eid; + if (eid == filament_id) + remapped = (replace_filament_id > 0) ? (size_t)replace_filament_id : 1; + else if (eid > filament_id) + remapped = eid - 1; + if (remapped > extruder_count) { + // filament_is_mixed is the pre-delete snapshot; index it with the ORIGINAL eid (1-based), + // not remapped, so we check whether this volume's current slot is a mixed slot. + bool is_mixed = !filament_is_mixed.empty() && eid >= 1 && (eid - 1) < filament_is_mixed.size() && filament_is_mixed[eid - 1]; + if (!is_mixed) + this->config.erase("extruder"); } } @@ -3495,6 +3589,15 @@ void FacetsAnnotation::get_facets(const ModelVolume& mv, std::vectorset(selector); +} + void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv, EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament, diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 2d46bc4cdf..6834c7a59b 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -47,6 +47,8 @@ namespace cereal { } namespace Slic3r { + +struct TexturedMesh; enum class ConversionType; class BuildVolume; @@ -740,6 +742,9 @@ public: EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE, EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE); + // Shift painted filament indices >= threshold by delta. Used when a physical filament is + // inserted ahead of existing slots (mixed-color slots are kept at the end of the list). + void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta); indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; bool empty() const { return m_data.triangles_to_split.empty(); } @@ -932,7 +937,8 @@ public: // BBS std::vector 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 &filament_is_mixed = {}); // Split this volume, append the result to the object owning this volume. // Return the number of volumes created from this one. @@ -1549,6 +1555,10 @@ public: std::shared_ptr model_info = nullptr; std::shared_ptr 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 texture_mesh; + //makerlab information std::string mk_name; std::string mk_version; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index a0fd9a9e04..908bdec2b3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1185,6 +1185,7 @@ static std::vector 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", diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index f1b653feac..b66c0a8587 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -8,6 +8,7 @@ #include "PresetCacheFormat.hpp" #include "PrintConfig.hpp" #include "PublishSettings.hpp" +#include "FilamentMixer.hpp" #include "libslic3r.h" #include "I18N.hpp" #include "Utils.hpp" @@ -41,7 +42,7 @@ // Store the print/filament/printer presets into a "presets" subdirectory of the Slic3rPE config dir. // This breaks compatibility with the upstream Slic3r if the --datadir is used to switch between the two versions. -//#define SLIC3R_PROFILE_USE_PRESETS_SUBDIR +// #define SLIC3R_PROFILE_USE_PRESETS_SUBDIR namespace Slic3r { @@ -49,35 +50,29 @@ namespace Slic3r { // below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync. // s_project_options_published additionally carries wipe_tower_rotation_angle, which normal // loads do not import (it is not listed here): published-only plate geometry. -static std::vector s_project_options { - "flush_volumes_vector", - "flush_volumes_matrix", - // BBS - "filament_colour", - "filament_colour_type", - "filament_multi_colour", - "wipe_tower_x", - "wipe_tower_y", - "curr_bed_type", - "flush_multiplier", - // Fast-purge mode: project-level purge control, inert at Default. - "flush_multiplier_fast", - "prime_volume_mode", - "nozzle_volume_type", - "filament_map_mode", - "filament_map", - // Per-filament nozzle-volume choice; project-level like filament_map so the per-filament - // slot resolution survives preset switches. - "filament_volume_map", - // Per-filament physical-nozzle choice the grouping engine writes back; project-level so a - // saved project round-trips the assignment alongside filament_map/filament_volume_map. - "filament_nozzle_map", - // Filament Track Switch device state: whether the switch is installed and ready, and - // whether dynamic per-nozzle filament mapping is active. Persisted with the project and - // restored from a saved 3mf; reset to false on load and set true only by live device sync. - "has_filament_switcher", - "enable_filament_dynamic_map" -}; +static std::vector s_project_options{"flush_volumes_vector", "flush_volumes_matrix", + // BBS + "filament_colour", "filament_colour_type", "filament_multi_colour", "wipe_tower_x", + "wipe_tower_y", "curr_bed_type", "flush_multiplier", + // Fast-purge mode: project-level purge control, inert at Default. + "flush_multiplier_fast", "prime_volume_mode", "nozzle_volume_type", "filament_map_mode", + "filament_map", + // Per-filament nozzle-volume choice; project-level like filament_map so the per-filament + // slot resolution survives preset switches. + "filament_volume_map", + // Per-filament physical-nozzle choice the grouping engine writes back; project-level so a + // saved project round-trips the assignment alongside filament_map/filament_volume_map. + "filament_nozzle_map", + // Filament Track Switch device state: whether the switch is installed and ready, and + // whether dynamic per-nozzle filament mapping is active. Persisted with the project and + // restored from a saved 3mf; reset to false on load and set true only by live device sync. + "has_filament_switcher", "enable_filament_dynamic_map", + // Mixed-color filament slots. Project-level parallel arrays indexed like filament_colour: + // which slots are virtual mixes, their component filaments, blend ratios and the optional + // Z-gradient description. Kept with the project so a saved 3mf round-trips the mix setup. + "filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios", + "filament_mixed_gradient", "filament_mixed_gradient_range", + "filament_mixed_gradient_curve", "filament_mixed_gradient_per_part"}; // Project options applied when loading a "published" 3MF project: s_project_options minus the // filament/purge keys, plus wipe_tower_rotation_angle (plate geometry that only published @@ -92,31 +87,26 @@ static std::vector s_project_options { // state (it must NOT be added). curr_bed_type is deliberately NOT in this list: the receiver // keeps its own bed type when loading a published project. The published-mode project_config // assertions in tests/libslic3r/test_preset_bundle_loading.cpp guard both directions. -static std::vector s_project_options_published { - "wipe_tower_x", - "wipe_tower_y", - "wipe_tower_rotation_angle" -}; +static std::vector s_project_options_published{"wipe_tower_x", "wipe_tower_y", "wipe_tower_rotation_angle"}; -//Orca: add custom as default -const char *PresetBundle::ORCA_DEFAULT_BUNDLE = "Custom"; -const char *PresetBundle::ORCA_DEFAULT_PRINTER_MODEL = "MyKlipper 0.4 nozzle"; -const char *PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT = "0.4"; -const char *PresetBundle::ORCA_DEFAULT_FILAMENT = "Generic PLA @System"; -const char *PresetBundle::ORCA_FILAMENT_LIBRARY = "OrcaFilamentLibrary"; -const char *PresetBundle::ORCA_DEFAULT_FILAMENT_PLACEHOLDER = "Default Filament"; +// Orca: add custom as default +const char* PresetBundle::ORCA_DEFAULT_BUNDLE = "Custom"; +const char* PresetBundle::ORCA_DEFAULT_PRINTER_MODEL = "MyKlipper 0.4 nozzle"; +const char* PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT = "0.4"; +const char* PresetBundle::ORCA_DEFAULT_FILAMENT = "Generic PLA @System"; +const char* PresetBundle::ORCA_FILAMENT_LIBRARY = "OrcaFilamentLibrary"; +const char* PresetBundle::ORCA_DEFAULT_FILAMENT_PLACEHOLDER = "Default Filament"; -DynamicPrintConfig PresetBundle::construct_full_config( - Preset& in_printer_preset, - Preset& in_print_preset, - const DynamicPrintConfig& project_config, - std::vector& in_filament_presets, - bool apply_extruder, - std::optional> filament_maps_new, - std::optional> filament_volume_maps_new) +DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset, + Preset& in_print_preset, + const DynamicPrintConfig& project_config, + std::vector& in_filament_presets, + bool apply_extruder, + std::optional> filament_maps_new, + std::optional> filament_volume_maps_new) { - DynamicPrintConfig &printer_config = in_printer_preset.config; - DynamicPrintConfig &print_config = in_print_preset.config; + DynamicPrintConfig& printer_config = in_printer_preset.config; + DynamicPrintConfig& print_config = in_print_preset.config; DynamicPrintConfig out; out.apply(FullPrintConfig::defaults()); @@ -128,7 +118,7 @@ DynamicPrintConfig PresetBundle::construct_full_config( size_t num_filaments = in_filament_presets.size(); std::vector filament_maps = out.option("filament_map")->values; - std::vector filament_volume_maps(num_filaments, (int)nvtStandard); + std::vector filament_volume_maps(num_filaments, (int) nvtStandard); ConfigOptionInts* filament_volume_map_opt = out.option("filament_volume_map"); if (filament_maps_new.has_value()) @@ -146,7 +136,7 @@ DynamicPrintConfig PresetBundle::construct_full_config( filament_volume_maps.resize(num_filaments, nvtStandard); } - auto *extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); + auto* extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector. std::vector compatible_printers_condition; std::vector compatible_prints_condition; @@ -155,11 +145,12 @@ DynamicPrintConfig PresetBundle::construct_full_config( std::vector print_compatible_printers; // BBS: add logic for settings check between different system presets std::vector different_settings; - std::string different_print_settings, different_printer_settings; + std::string different_print_settings, different_printer_settings; compatible_printers_condition.emplace_back(in_print_preset.compatible_printers_condition()); - const ConfigOptionStrings *compatible_printers = print_config.option("compatible_printers", false); - if (compatible_printers) print_compatible_printers = compatible_printers->values; + const ConfigOptionStrings* compatible_printers = print_config.option("compatible_printers", false); + if (compatible_printers) + print_compatible_printers = compatible_printers->values; // BBS: add logic for settings check between different system presets std::string print_inherits = in_print_preset.inherits(); inherits.emplace_back(print_inherits); @@ -169,7 +160,7 @@ DynamicPrintConfig PresetBundle::construct_full_config( int extruder_count = 1, extruder_volume_type_count = 1; bool different_extruder = false; if (apply_extruder) { - different_extruder = out.support_different_extruders(extruder_count); + different_extruder = out.support_different_extruders(extruder_count); extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); if ((extruder_count > 1) || different_extruder) { @@ -181,10 +172,13 @@ DynamicPrintConfig PresetBundle::construct_full_config( // per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing // path composes variant_2 first and is unaffected; changing the order here would alter // long-standing composed values, so any fix must re-baseline them. - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); // update print config related with variants - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); } } @@ -192,7 +186,9 @@ DynamicPrintConfig PresetBundle::construct_full_config( // BBS: update filament config related with variants DynamicPrintConfig filament_config = in_filament_presets[0].config; if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); + filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + filament_options_with_variant, "", "filament_extruder_variant", 1, + filament_maps[0], (NozzleVolumeType) filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition()); compatible_prints_condition.emplace_back(in_filament_presets[0].compatible_prints_condition()); @@ -200,13 +196,13 @@ DynamicPrintConfig PresetBundle::construct_full_config( inherits.emplace_back(filament_inherits); filament_ids.emplace_back(in_filament_presets[0].filament_id); - std::vector &filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + std::vector& filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); } else { - std::vector filament_configs; - std::vector filament_presets; - for (const Preset & preset : in_filament_presets) { + std::vector filament_configs; + std::vector filament_presets; + for (const Preset& preset : in_filament_presets) { filament_presets.emplace_back(&preset); filament_configs.emplace_back(&(preset.config)); } @@ -216,37 +212,45 @@ DynamicPrintConfig PresetBundle::construct_full_config( for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); + filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, + nozzle_volume_types, filament_options_with_variant, "", + "filament_extruder_variant", 1, filament_maps[i], + (NozzleVolumeType) filament_volume_maps[i]); } // loop through options and apply them to the resulting config. std::vector filament_variant_count(num_filaments, 1); - for (const t_config_option_key &key : in_filament_presets[0].config.keys()) { - if (key == "compatible_prints" || key == "compatible_printers") continue; + for (const t_config_option_key& key : in_filament_presets[0].config.keys()) { + if (key == "compatible_prints" || key == "compatible_printers") + continue; // Get a destination option. - ConfigOption *opt_dst = out.option(key, false); + ConfigOption* opt_dst = out.option(key, false); if (opt_dst->is_scalar()) { // Get an option, do not create if it does not exist. - const ConfigOption *opt_src = filament_temp_configs.front().option(key); - if (opt_src != nullptr) opt_dst->set(opt_src); + const ConfigOption* opt_src = filament_temp_configs.front().option(key); + if (opt_src != nullptr) + opt_dst->set(opt_src); } else { // BBS - ConfigOptionVectorBase *opt_vec_dst = static_cast(opt_dst); + ConfigOptionVectorBase* opt_vec_dst = static_cast(opt_dst); { if (apply_extruder) { - std::vector filament_opts(num_filaments, nullptr); + std::vector filament_opts(num_filaments, nullptr); // Setting a vector value from all filament_configs. - for (size_t i = 0; i < filament_opts.size(); ++i) filament_opts[i] = filament_temp_configs[i].option(key); + for (size_t i = 0; i < filament_opts.size(); ++i) + filament_opts[i] = filament_temp_configs[i].option(key); opt_vec_dst->set(filament_opts); } else { for (size_t i = 0; i < num_filaments; ++i) { - const ConfigOptionVectorBase *filament_option = static_cast(filament_temp_configs[i].option(key)); + const ConfigOptionVectorBase* filament_option = static_cast( + filament_temp_configs[i].option(key)); if (i == 0) opt_vec_dst->set(filament_option); else opt_vec_dst->append(filament_option); - if (key == "filament_extruder_variant") filament_variant_count[i] = filament_option->size(); + if (key == "filament_extruder_variant") + filament_variant_count[i] = filament_option->size(); } } } @@ -255,12 +259,14 @@ DynamicPrintConfig PresetBundle::construct_full_config( if (!apply_extruder) { // append filament_self_index - std::vector &filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + std::vector& filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); int k = 0; for (size_t i = 0; i < num_filaments; i++) { - for (size_t j = 0; j < filament_variant_count[i]; j++) { filament_self_indice[k++] = i + 1; } + for (size_t j = 0; j < filament_variant_count[i]; j++) { + filament_self_indice[k++] = i + 1; + } } } } @@ -274,10 +280,10 @@ DynamicPrintConfig PresetBundle::construct_full_config( // BBS: add logic for settings check between different system presets out.erase("different_settings_to_system"); - static const char *keys[] = {"support_filament", "support_interface_filament"}; + static const char* keys[] = {"support_filament", "support_interface_filament"}; for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { std::string key = std::string(keys[i]); - auto *opt = dynamic_cast(out.option(key, false)); + auto* opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); } @@ -293,27 +299,28 @@ DynamicPrintConfig PresetBundle::construct_full_config( out.option("filament_map", true)->values = filament_maps; out.option("filament_volume_map", true)->values = filament_volume_maps; - auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { + auto add_if_some_non_empty = [&out](std::vector&& values, const std::string& key) { bool nonempty = false; - for (const std::string &v : values) + for (const std::string& v : values) if (!v.empty()) { nonempty = true; break; } - if (nonempty) out.set_key_value(key, new ConfigOptionStrings(std::move(values))); + if (nonempty) + out.set_key_value(key, new ConfigOptionStrings(std::move(values))); }; add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); add_if_some_non_empty(std::move(inherits), "inherits_group"); // BBS: add logic for settings check between different system presets - //add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); + // add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); out.option("printer_technology", true)->value = ptFFF; return out; } -std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Preset::Type type) +std::string PresetBundle::find_preset_vendor(const std::string& preset_name, Preset::Type type) { // Get the resources preset directory (contains all bundled vendor profiles) fs::path system_dir = fs::path(Slic3r::resources_dir()) / PRESET_PROFILES_DIR; @@ -339,10 +346,9 @@ std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Pre // instead of the raw profile JSONs, by its cache alone. for (const std::string& vendor_name : vendor_names_in(system_dir)) { const fs::path vendor_json = system_dir / (vendor_name + ".json"); - if (! fs::exists(vendor_json)) { + if (!fs::exists(vendor_json)) { if (VendorCacheFile::carries_preset((system_dir / (vendor_name + ".opc")).string(), vendor_name, type, preset_name)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << preset_name - << " in vendor cache " << vendor_name; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << preset_name << " in vendor cache " << vendor_name; return vendor_name; } continue; @@ -378,13 +384,11 @@ std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Pre continue; // Found the preset! Get the vendor name and install the entire bundle - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << p_name - << " in vendor bundle " << vendor_name; - + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << p_name << " in vendor bundle " << vendor_name; + return vendor_name; } - } - catch (const std::exception &e) { + } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to find vendor name for " << preset_name << ": " << e.what(); return ""; } @@ -395,11 +399,21 @@ std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Pre } PresetBundle::PresetBundle() - : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) - , filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast(FullPrintConfig::defaults()), ORCA_DEFAULT_FILAMENT_PLACEHOLDER) - , sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast(SLAFullPrintConfig::defaults())) - , sla_prints(Preset::TYPE_SLA_PRINT, Preset::sla_print_options(), static_cast(SLAFullPrintConfig::defaults())) - , printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast(FullPrintConfig::defaults()), "Default Printer") + : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) + , filaments(Preset::TYPE_FILAMENT, + Preset::filament_options(), + static_cast(FullPrintConfig::defaults()), + ORCA_DEFAULT_FILAMENT_PLACEHOLDER) + , sla_materials(Preset::TYPE_SLA_MATERIAL, + Preset::sla_material_options(), + static_cast(SLAFullPrintConfig::defaults())) + , sla_prints(Preset::TYPE_SLA_PRINT, + Preset::sla_print_options(), + static_cast(SLAFullPrintConfig::defaults())) + , printers(Preset::TYPE_PRINTER, + Preset::printer_options(), + static_cast(FullPrintConfig::defaults()), + "Default Printer") , physical_printers(PhysicalPrinter::printer_options()) { // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes, @@ -421,12 +435,12 @@ PresetBundle::PresetBundle() // Set all the nullable values to nils. { auto& default_config = this->filaments.default_preset().config; - for(const std::string& opt_key : default_config.keys()){ - ConfigOption* opt = default_config.optptr(opt_key, false); + for (const std::string& opt_key : default_config.keys()) { + ConfigOption* opt = default_config.optptr(opt_key, false); bool is_override_key = is_filament_extruder_override_key(opt_key); - if(!is_override_key || !opt->nullable()) + if (!is_override_key || !opt->nullable()) continue; - opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable); + opt->deserialize("nil", ForwardCompatibilitySubstitutionRule::Disable); } } @@ -439,15 +453,16 @@ PresetBundle::PresetBundle() this->sla_prints.default_preset().compatible_printers_condition(); this->sla_prints.default_preset().inherits(); - //this->printers.add_default_preset(Preset::sla_printer_options(), static_cast(SLAFullPrintConfig::defaults()), "- default SLA -"); - //this->printers.preset(1).printer_technology_ref() = ptSLA; + // this->printers.add_default_preset(Preset::sla_printer_options(), static_cast(SLAFullPrintConfig::defaults()), "- default SLA -"); this->printers.preset(1).printer_technology_ref() = ptSLA; for (size_t i = 0; i < 1; ++i) { // The following ugly switch is to avoid printers.preset(0) to return the edited instance, as the 0th default is the current one. - Preset &preset = this->printers.default_preset(i); - for (const char *key : {"printer_settings_id", "printer_model", "printer_variant", "thumbnails"}) preset.config.optptr(key, true); - //if (i == 0) { - preset.config.optptr("default_print_profile", true); - preset.config.option("default_filament_profile", true); + Preset& preset = this->printers.default_preset(i); + for (const char* key : {"printer_settings_id", "printer_model", "printer_variant", "thumbnails"}) + preset.config.optptr(key, true); + // if (i == 0) { + preset.config.optptr("default_print_profile", true); + preset.config.option("default_filament_profile", true); //} else { // preset.config.optptr("default_sla_print_profile", true); // preset.config.optptr("default_sla_material_profile", true); @@ -466,32 +481,29 @@ PresetBundle::PresetBundle() this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options); } -PresetBundle::PresetBundle(const PresetBundle &rhs) -{ - *this = rhs; -} +PresetBundle::PresetBundle(const PresetBundle& rhs) { *this = rhs; } -PresetBundle& PresetBundle::operator=(const PresetBundle &rhs) +PresetBundle& PresetBundle::operator=(const PresetBundle& rhs) { - prints = rhs.prints; - sla_prints = rhs.sla_prints; - filaments = rhs.filaments; - sla_materials = rhs.sla_materials; - printers = rhs.printers; - physical_printers = rhs.physical_printers; + prints = rhs.prints; + sla_prints = rhs.sla_prints; + filaments = rhs.filaments; + sla_materials = rhs.sla_materials; + printers = rhs.printers; + physical_printers = rhs.physical_printers; - filament_presets = rhs.filament_presets; - project_config = rhs.project_config; - vendors = rhs.vendors; - obsolete_presets = rhs.obsolete_presets; - m_errors = rhs.m_errors; + filament_presets = rhs.filament_presets; + project_config = rhs.project_config; + vendors = rhs.vendors; + obsolete_presets = rhs.obsolete_presets; + m_errors = rhs.m_errors; // Adjust Preset::vendor pointers to point to the copied vendors map. - prints .update_vendor_ptrs_after_copy(this->vendors); - sla_prints .update_vendor_ptrs_after_copy(this->vendors); - filaments .update_vendor_ptrs_after_copy(this->vendors); + prints.update_vendor_ptrs_after_copy(this->vendors); + sla_prints.update_vendor_ptrs_after_copy(this->vendors); + filaments.update_vendor_ptrs_after_copy(this->vendors); sla_materials.update_vendor_ptrs_after_copy(this->vendors); - printers .update_vendor_ptrs_after_copy(this->vendors); + printers.update_vendor_ptrs_after_copy(this->vendors); return *this; } @@ -500,13 +512,13 @@ void PresetBundle::reset(bool delete_files) { // Clear the existing presets, delete their respective files. this->vendors.clear(); - this->prints .reset(delete_files); - this->sla_prints .reset(delete_files); - this->filaments .reset(delete_files); + this->prints.reset(delete_files); + this->sla_prints.reset(delete_files); + this->filaments.reset(delete_files); this->sla_materials.reset(delete_files); - this->printers .reset(delete_files); + this->printers.reset(delete_files); // BBS: filament_presets is load from project config, not handled here - //this->filament_presets.clear(); + // this->filament_presets.clear(); if (this->filament_presets.empty()) this->filament_presets.emplace_back(this->filaments.get_selected_preset_name()); this->obsolete_presets.prints.clear(); @@ -519,26 +531,25 @@ void PresetBundle::reset(bool delete_files) void PresetBundle::setup_directories() { boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); - //BBS: change directoties by design + // BBS: change directoties by design std::initializer_list paths = { data_dir, data_dir / "ota", - data_dir / PRESET_SYSTEM_DIR, + data_dir / PRESET_SYSTEM_DIR, data_dir / PRESET_USER_DIR, // Store the print/filament/printer presets at the same location as the upstream Slic3r. - //data_dir / PRESET_SYSTEM_DIR / PRESET_PRINT_NAME, - //data_dir / PRESET_SYSTEM_DIR / PRESET_FILAMENT_NAME, - //data_dir / PRESET_SYSTEM_DIR / PRESET_PRINTER_NAME + // data_dir / PRESET_SYSTEM_DIR / PRESET_PRINT_NAME, + // data_dir / PRESET_SYSTEM_DIR / PRESET_FILAMENT_NAME, + // data_dir / PRESET_SYSTEM_DIR / PRESET_PRINTER_NAME }; - for (const boost::filesystem::path &path : paths) { - boost::filesystem::path subdir = path; + for (const boost::filesystem::path& path : paths) { + boost::filesystem::path subdir = path; subdir.make_preferred(); - if (! boost::filesystem::is_directory(subdir) && - ! boost::filesystem::create_directory(subdir)) { + if (!boost::filesystem::is_directory(subdir) && !boost::filesystem::create_directory(subdir)) { if (boost::filesystem::is_directory(subdir)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!")%subdir.string(); - } - else + BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!") % + subdir.string(); + } else throw Slic3r::RuntimeError(std::string("Unable to create directory ") + subdir.string()); } } @@ -547,7 +558,7 @@ void PresetBundle::setup_directories() // recursively copy all files and dirs in from_dir to to_dir static void copy_dir(const boost::filesystem::path& from_dir, const boost::filesystem::path& to_dir) { - if(!boost::filesystem::is_directory(from_dir)) + if (!boost::filesystem::is_directory(from_dir)) return; // i assume to_dir.parent surely exists if (!boost::filesystem::is_directory(to_dir)) @@ -571,33 +582,33 @@ void PresetBundle::copy_files(const std::string& from) // list of searched paths based on current directory system in setup_directories() // do not copy cache and snapshots boost::filesystem::path from_data_dir = boost::filesystem::path(from); - //BBS: change directoties by design - std::initializer_list from_dirs= { - //from_data_dir / "vendor", - // Store the print/filament/printer presets at the same location as the upstream Slic3r. - from_data_dir / PRESET_PRINT_NAME, - from_data_dir / PRESET_FILAMENT_NAME, - from_data_dir / PRESET_PRINTER_NAME - }; + // BBS: change directoties by design + std::initializer_list from_dirs = + {// from_data_dir / "vendor", + // Store the print/filament/printer presets at the same location as the upstream Slic3r. + from_data_dir / PRESET_PRINT_NAME, from_data_dir / PRESET_FILAMENT_NAME, from_data_dir / PRESET_PRINTER_NAME}; // copy recursively all files - //BBS: change directoties by design + // BBS: change directoties by design for (const boost::filesystem::path& from_dir : from_dirs) { - copy_dir(from_dir, data_dir /"old"/from_dir.filename()); + copy_dir(from_dir, data_dir / "old" / from_dir.filename()); } } -PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, - const PresetPreferences& preferred_selection/* = PresetPreferences()*/) +PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig& config, + ForwardCompatibilitySubstitutionRule substitution_rule, + const PresetPreferences& preferred_selection /* = PresetPreferences()*/) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%") % substitution_rule % + preferred_selection.printer_model_id; const auto startup_t0 = std::chrono::steady_clock::now(); - //BBS: change system config to json + // BBS: change system config to json std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); // BBS load preset from user's folder, load system default if @@ -623,35 +634,26 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward set_calibrate_printer(""); { - const auto total_ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - startup_t0).count(); + const auto total_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - startup_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: all presets loaded in " << total_ms << " ms"; } - //BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); + // BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%") % substitutions.size(); return substitutions; } -//BBS: add function to generate differed preset for save -//the pointer should be freed by the caller +// BBS: add function to generate differed preset for save +// the pointer should be freed by the caller Preset* PresetBundle::get_preset_differed_for_save(Preset& preset) { PresetCollection* preset_collection; - switch(preset.type) { - case Preset::TYPE_PRINT: - preset_collection = &(this->prints); - break; - case Preset::TYPE_PRINTER: - preset_collection = &(this->printers); - break; - case Preset::TYPE_FILAMENT: - preset_collection = &(this->filaments); - break; - default: - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly")%preset.type; - return nullptr; + switch (preset.type) { + case Preset::TYPE_PRINT: preset_collection = &(this->prints); break; + case Preset::TYPE_PRINTER: preset_collection = &(this->printers); break; + case Preset::TYPE_FILAMENT: preset_collection = &(this->filaments); break; + default: BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly") % preset.type; return nullptr; } return preset_collection->get_preset_differed_for_save(preset); @@ -661,25 +663,17 @@ int PresetBundle::get_differed_values_to_update(Preset& preset, std::mapprints); - break; - case Preset::TYPE_PRINTER: - preset_collection = &(this->printers); - break; - case Preset::TYPE_FILAMENT: - preset_collection = &(this->filaments); - break; - default: - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly")%preset.type; - return -1; + switch (preset.type) { + case Preset::TYPE_PRINT: preset_collection = &(this->prints); break; + case Preset::TYPE_PRINTER: preset_collection = &(this->printers); break; + case Preset::TYPE_FILAMENT: preset_collection = &(this->filaments); break; + default: BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly") % preset.type; return -1; } return preset_collection->get_differed_values_to_update(preset, key_values); } -//BBS: get vendor's current version +// BBS: get vendor's current version Semver PresetBundle::get_vendor_profile_version(std::string vendor_name) { Semver result_ver; @@ -694,12 +688,11 @@ Semver PresetBundle::get_vendor_profile_version(std::string vendor_name) VendorType PresetBundle::get_current_vendor_type() { - auto t = VendorType::Unknown; - auto config = &printers.get_edited_preset().config; + auto t = VendorType::Unknown; + auto config = &printers.get_edited_preset().config; const auto* printer_model = config->opt("printer_model"); if (printer_model == nullptr) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is " - << (config->has("printer_model") ? "not a string" : "missing") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is " << (config->has("printer_model") ? "not a string" : "missing") << ", vendor type is Unknown"; return t; } @@ -715,12 +708,11 @@ VendorType PresetBundle::get_current_vendor_type() if (!vendor_name.empty()) break; } - if (!vendor_name.empty()) - { - if(vendor_name.compare("BBL") == 0) + if (!vendor_name.empty()) { + if (vendor_name.compare("BBL") == 0) t = VendorType::Marlin_BBL; - - if(vendor_name.compare("Qidi") == 0) + + if (vendor_name.compare("Qidi") == 0) t = VendorType::Klipper_Qidi; } return t; @@ -733,7 +725,8 @@ bool PresetBundle::use_bbl_network() return use_bbl_network; } -bool PresetBundle::use_bbl_device_tab() { +bool PresetBundle::use_bbl_device_tab() +{ if (!is_bbl_vendor()) { return false; } @@ -743,7 +736,7 @@ bool PresetBundle::use_bbl_device_tab() { } const auto cfg = printers.get_edited_preset().config; - // Use bbl device tab if printhost webui url is not set + // Use bbl device tab if printhost webui url is not set return cfg.opt_string("print_host_webui").empty(); } @@ -770,7 +763,8 @@ bool PresetBundle::backup_user_folder() const } } -std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const +std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, + const std::string& printer_name) const { if (filament_id.empty()) return std::nullopt; @@ -780,11 +774,11 @@ std::optional PresetBundle::get_filament_by_filament_id(const for (auto iter = filaments.begin(); iter != filaments.end(); ++iter) { const Preset& filament_preset = *iter; - const auto& config = filament_preset.config; + const auto& config = filament_preset.config; if (filament_preset.filament_id == filament_id) { FilamentBaseInfo info; - info.filament_id = filament_id; - info.is_system = filament_preset.is_system; + info.filament_id = filament_id; + info.is_system = filament_preset.is_system; info.filament_name = filament_preset.alias; if (config.has("filament_is_support")) info.is_support = config.option("filament_is_support")->values[0]; @@ -796,7 +790,7 @@ std::optional PresetBundle::get_filament_by_filament_id(const info.nozzle_temp_range_high = config.option("nozzle_temperature_range_high")->values[0]; if (config.has("nozzle_temperature_range_low")) info.nozzle_temp_range_low = config.option("nozzle_temperature_range_low")->values[0]; - if(config.has("temperature_vitrification")) + if (config.has("temperature_vitrification")) info.temperature_vitrification = config.option("temperature_vitrification")->values[0]; if (!printer_name.empty()) { @@ -805,11 +799,11 @@ std::optional PresetBundle::get_filament_by_filament_id(const if (iter != compatible_printers.end() && config.has("filament_printable")) { info.filament_printable = config.option("filament_printable")->values[0]; if (config.has("filament_extruder_compatibility")) - info.set_filament_extruder_compatibility(config.option("filament_extruder_compatibility")->values[0]); + info.set_filament_extruder_compatibility( + config.option("filament_extruder_compatibility")->values[0]); return info; } - } - else { + } else { return info; } } @@ -817,44 +811,47 @@ std::optional PresetBundle::get_filament_by_filament_id(const return std::nullopt; } -//BBS: load project embedded presets -PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule) +// BBS: load project embedded presets +PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector project_presets, + ForwardCompatibilitySubstitutionRule substitution_rule) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preset toltal count %2%")%substitution_rule% project_presets.size(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(" enter, substitution_rule %1%, preset toltal count %2%") % substitution_rule % + project_presets.size(); try { this->prints.load_project_embedded_presets(project_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); - } catch (const std::runtime_error &err) { + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } try { this->filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); - } catch (const std::runtime_error &err) { + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } try { this->printers.load_project_embedded_presets(project_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); - } catch (const std::runtime_error &err) { + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } - //this->update_multi_material_filament_presets(); - //this->update_compatible(PresetSelectCompatibleType::Never); - // Rewrite renamed compatible references before the caller (Plater) selects the project presets. + // this->update_multi_material_filament_presets(); + // this->update_compatible(PresetSelectCompatibleType::Never); + // Rewrite renamed compatible references before the caller (Plater) selects the project presets. this->normalize_compatible_presets(); - if (! errors_cummulative.empty()) + if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); - //this->load_selections(config, ""); + // this->load_selections(config, ""); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%") % substitutions.size(); return substitutions; } -//BBS: get current project embedded presets +// BBS: get current project embedded presets std::vector PresetBundle::get_current_project_embedded_presets() { std::vector project_presets; @@ -868,39 +865,38 @@ std::vector PresetBundle::get_current_project_embedded_presets() if (!printer_presets.empty()) std::copy(printer_presets.begin(), printer_presets.end(), std::back_inserter(project_presets)); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned project_presets count %1%")%project_presets.size(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned project_presets count %1%") % project_presets.size(); return project_presets; } -//BBS: reset project embedded presets +// BBS: reset project embedded presets void PresetBundle::reset_project_embedded_presets() { std::string prefer_printer; Preset& current_printer = this->printers.get_selected_preset(); - ConfigOption* inherits = current_printer.config.option("inherits"); + ConfigOption* inherits = current_printer.config.option("inherits"); if (inherits) { - prefer_printer = dynamic_cast(inherits)->value; + prefer_printer = dynamic_cast(inherits)->value; } - //first printer, then filament, then print - bool printer_reselect = this->printers.reset_project_embedded_presets(); + // first printer, then filament, then print + bool printer_reselect = this->printers.reset_project_embedded_presets(); bool filament_reselect = this->filaments.reset_project_embedded_presets(); - bool print_reselect = this->prints.reset_project_embedded_presets(); + bool print_reselect = this->prints.reset_project_embedded_presets(); if (printer_reselect) { if (!prefer_printer.empty()) - this->printers.select_preset_by_name(prefer_printer, true); + this->printers.select_preset_by_name(prefer_printer, true); else - this->printers.select_preset(this->printers.first_visible_idx()); + this->printers.select_preset(this->printers.first_visible_idx()); - //this->update_multi_material_filament_presets(); + // this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); - } - else if (filament_reselect || print_reselect) { - //Preset& current_printer = this->printers.get_selected_preset(); + } else if (filament_reselect || print_reselect) { + // Preset& current_printer = this->printers.get_selected_preset(); /*if (filament_reselect) { - const std::vector &prefered_filament_profiles = current_printer.config.option("default_filament_profile")->values; - const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); - if (!prefered_filament_profile.empty()) + const std::vector &prefered_filament_profiles = + current_printer.config.option("default_filament_profile")->values; const std::string prefered_filament_profile + = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); if (!prefered_filament_profile.empty()) this->filaments.select_preset_by_name(prefered_filament_profile, true); else this->filaments.select_preset(this->filaments.first_visible_idx()); @@ -911,17 +907,18 @@ void PresetBundle::reset_project_embedded_presets() this->update_compatible(PresetSelectCompatibleType::Never); } - //this->update_multi_material_filament_presets(); + // this->update_multi_material_filament_presets(); - //update filament_presets - for (size_t i = 0; i < filament_presets.size(); ++ i) - { + // update filament_presets + for (size_t i = 0; i < filament_presets.size(); ++i) { Preset* selected_filament = this->filaments.find_preset(filament_presets[i], false); if (!selected_filament) { - //it should be the project embedded presets + // it should be the project embedded presets Preset& current_printer = this->printers.get_selected_preset(); - const std::vector &prefered_filament_profiles = current_printer.config.option("default_filament_profile")->values; - const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); + const std::vector& prefered_filament_profiles = + current_printer.config.option("default_filament_profile")->values; + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : + prefered_filament_profiles.front(); if (!prefered_filament_profile.empty()) { // Check if preferred filament exists and is visible const Preset* preferred_preset = this->filaments.find_preset(prefered_filament_profile, false); @@ -937,26 +934,22 @@ void PresetBundle::reset_project_embedded_presets() } } -//BBS: get bed texture for printer model +// BBS: get bed texture for printer model std::string PresetBundle::get_texture_for_printer_model(std::string model_name) { std::string texture_name, vendor_name, out; - for (auto vendor_profile: this->vendors) - { - for (auto vendor_model: vendor_profile.second.models) - { - if (vendor_model.name == model_name || vendor_model.id == model_name) - { + for (auto vendor_profile : this->vendors) { + for (auto vendor_model : vendor_profile.second.models) { + if (vendor_model.name == model_name || vendor_model.id == model_name) { texture_name = vendor_model.bed_texture; - vendor_name = vendor_profile.first; + vendor_name = vendor_profile.first; break; } } } - if (!texture_name.empty()) - { + if (!texture_name.empty()) { out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + texture_name; if (!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + texture_name; @@ -965,26 +958,22 @@ std::string PresetBundle::get_texture_for_printer_model(std::string model_name) return out; } -//BBS: get stl model for printer model +// BBS: get stl model for printer model std::string PresetBundle::get_stl_model_for_printer_model(std::string model_name) { std::string stl_name, vendor_name, out; - for (auto vendor_profile: this->vendors) - { - for (auto vendor_model: vendor_profile.second.models) - { - if (vendor_model.name == model_name) - { - stl_name = vendor_model.bed_model; + for (auto vendor_profile : this->vendors) { + for (auto vendor_model : vendor_profile.second.models) { + if (vendor_model.name == model_name) { + stl_name = vendor_model.bed_model; vendor_name = vendor_profile.first; break; } } } - if (!stl_name.empty()) - { + if (!stl_name.empty()) { out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + stl_name; if (!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + stl_name; @@ -997,27 +986,23 @@ std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_n { std::string hotend_stl, vendor_name, out; - for (auto vendor_profile: this->vendors) - { - for (auto vendor_model: vendor_profile.second.models) - { - if (vendor_model.name == model_name) - { - hotend_stl = vendor_model.hotend_model; + for (auto vendor_profile : this->vendors) { + for (auto vendor_model : vendor_profile.second.models) { + if (vendor_model.name == model_name) { + hotend_stl = vendor_model.hotend_model; vendor_name = vendor_profile.first; break; } } } - if (!hotend_stl.empty()) - { + if (!hotend_stl.empty()) { out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + hotend_stl; if (!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + hotend_stl; } - if (out.empty() ||!boost::filesystem::exists(boost::filesystem::path(out))) + if (out.empty() || !boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/hotend.stl"; return out; @@ -1030,11 +1015,13 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For std::string errors_cummulative; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); - if (!fs::exists(user_folder)) fs::create_directory(user_folder); + if (!fs::exists(user_folder)) + fs::create_directory(user_folder); std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user; - fs::path folder(user_folder / user); - if (!fs::exists(folder)) fs::create_directory(folder); + fs::path folder(user_folder / user); + if (!fs::exists(folder)) + fs::create_directory(folder); bundles.WriteLock(); bundles.m_bundles.clear(); @@ -1047,30 +1034,36 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For if (fs::exists(local_dir)) { dir_user_presets_local = local_dir; for (auto& entry : fs::directory_iterator(local_dir)) { - if (!fs::is_directory(entry.path())) continue; + if (!fs::is_directory(entry.path())) + continue; std::string bundle_dir = entry.path().string(); fs::path metadata_file = entry.path() / PRESET_BUNDLE_METADATA; - if (!fs::exists(metadata_file)) continue; + if (!fs::exists(metadata_file)) + continue; BundleMetadata metadata; - if (!metadata.load_from_json(metadata_file.string())) continue; + if (!metadata.load_from_json(metadata_file.string())) + continue; metadata.print_presets.clear(); metadata.filament_presets.clear(); metadata.printer_presets.clear(); - this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { - metadata.print_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); - this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { - metadata.filament_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); - this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { - metadata.printer_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->prints.load_presets( + bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, + [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, + PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->filaments.load_presets( + bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, + [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); }, + PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->printers.load_presets( + bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, + [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); }, + PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); metadata.bundle_type = BundleType::Local; - metadata.path = metadata_file.string(); + metadata.path = metadata_file.string(); bundles.WriteLock(); bundles.m_bundles[metadata.id] = metadata; @@ -1082,32 +1075,38 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For fs::path subscribed_dir(folder / PRESET_SUBSCRIBED_DIR); if (fs::exists(subscribed_dir)) { for (auto& entry : fs::directory_iterator(subscribed_dir)) { - if (!fs::is_directory(entry.path())) continue; + if (!fs::is_directory(entry.path())) + continue; std::string bundle_dir = entry.path().string(); fs::path metadata_file = entry.path() / PRESET_BUNDLE_METADATA; - if (!fs::exists(metadata_file)) continue; + if (!fs::exists(metadata_file)) + continue; BundleMetadata metadata; - if (!metadata.load_from_json(metadata_file.string())) continue; + if (!metadata.load_from_json(metadata_file.string())) + continue; metadata.print_presets.clear(); metadata.filament_presets.clear(); metadata.printer_presets.clear(); metadata.is_subscribed = true; - this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { - metadata.print_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); - this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { - metadata.filament_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); - this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { - metadata.printer_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->prints.load_presets( + bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, + [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, + PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->filaments.load_presets( + bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, + [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); }, + PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->printers.load_presets( + bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, + [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); }, + PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); - metadata.bundle_type = BundleType::Subscribed; - metadata.path = metadata_file.string(); + metadata.bundle_type = BundleType::Subscribed; + metadata.path = metadata_file.string(); metadata.update_available = false; bundles.WriteLock(); @@ -1125,27 +1124,32 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For std::string sel = prints.get_selected_preset().name; this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); prints.select_preset_by_name(sel, false); - } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + } catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + } try { std::string sel = filaments.get_selected_preset().name; this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); filaments.select_preset_by_name(sel, false); - } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + } catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + } try { std::string sel = printers.get_selected_preset().name; this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); printers.select_preset_by_name(sel, false); - } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } - if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); + } catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + } + if (!errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); - const auto json_ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - json_t0).count(); + const auto json_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - json_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets loaded from JSON in " << json_ms << " ms"; } { - const auto ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - user_load_t0).count(); + const auto ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - user_load_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: user + bundle presets loaded in " << ms << " ms"; } @@ -1156,62 +1160,70 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For return PresetsConfigSubstitutions(); } -PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & config, - std::map> &my_presets, - ForwardCompatibilitySubstitutionRule substitution_rule) +PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig& config, + std::map>& my_presets, + ForwardCompatibilitySubstitutionRule substitution_rule) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; bool process_added = false, filament_added = false, machine_added = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preset toltal count %2%")%substitution_rule%my_presets.size(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" print's selected_idx %1%, selected_name %2%") %prints.get_selected_idx() %prints.get_selected_preset_name(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" filament's selected_idx %1%, selected_name %2%") %filaments.get_selected_idx() %filaments.get_selected_preset_name(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" printers's selected_idx %1%, selected_name %2%") %printers.get_selected_idx() %printers.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" enter, substitution_rule %1%, preset toltal count %2%") % substitution_rule % + my_presets.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" print's selected_idx %1%, selected_name %2%") % prints.get_selected_idx() % + prints.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" filament's selected_idx %1%, selected_name %2%") % filaments.get_selected_idx() % + filaments.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" printers's selected_idx %1%, selected_name %2%") % printers.get_selected_idx() % + printers.get_selected_preset_name(); // Sync removing remove_users_preset(config, &my_presets); std::map>::iterator it; for (int pass = 0; pass < 2; ++pass) - for (it = my_presets.begin(); it != my_presets.end(); it++) { - std::string name = it->first; - std::map& value_map = it->second; - // Load user root presets at first pass - std::map::iterator inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); - if ((pass == 1) == (inherits_iter == value_map.end() || inherits_iter->second.empty())) - continue; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " start load from cloud: " << name; - //get the type first - std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); - if (type_iter == value_map.end()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%")%name; - continue; - } - try { - PresetCollection *preset_collection = nullptr; - if (type_iter->second == PRESET_IOT_PRINT_TYPE) { - preset_collection = &(this->prints); - process_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); - } - else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { - preset_collection = &(this->filaments); - filament_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); - } - else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { - preset_collection = &(this->printers); - machine_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); - } - else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") %type_iter->second %name; + for (it = my_presets.begin(); it != my_presets.end(); it++) { + std::string name = it->first; + std::map& value_map = it->second; + // Load user root presets at first pass + std::map::iterator inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); + if ((pass == 1) == (inherits_iter == value_map.end() || inherits_iter->second.empty())) + continue; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " start load from cloud: " << name; + // get the type first + std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); + if (type_iter == value_map.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%") % name; continue; } + try { + PresetCollection* preset_collection = nullptr; + if (type_iter->second == PRESET_IOT_PRINT_TYPE) { + preset_collection = &(this->prints); + process_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, + PresetOrigin(PresetOrigin::Kind::User)); + } else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { + preset_collection = &(this->filaments); + filament_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, + PresetOrigin(PresetOrigin::Kind::User)); + } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + machine_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, + PresetOrigin(PresetOrigin::Kind::User)); + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format("invalid type %1% for setting %2%") % type_iter->second % name; + continue; + } + } catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + } } - catch (const std::runtime_error& err) { - errors_cummulative += err.what(); - } - } /*if (process_added) { this->prints.update_after_user_presets_loaded(); } @@ -1227,37 +1239,38 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); - //this->load_selections(config, PresetPreferences()); + // this->load_selections(config, PresetPreferences()); set_calibrate_printer(""); - if (! errors_cummulative.empty()) + if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, process_added %1%, filament_added %2%, machine_added %3%")%process_added %filament_added %machine_added; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(" finished, process_added %1%, filament_added %2%, machine_added %3%") % process_added % + filament_added % machine_added; return substitutions; } -bool PresetBundle::apply_vendor_config( - const std::map>>& new_vendors, - const std::map& new_filaments, - AppConfig* app_config, - bool overwrite, - const std::string& preferred_printer_model, - const std::string& preferred_printer_variant, - const std::string& preferred_filament) +bool PresetBundle::apply_vendor_config(const std::map>>& new_vendors, + const std::map& new_filaments, + AppConfig* app_config, + bool overwrite, + const std::string& preferred_printer_model, + const std::string& preferred_printer_variant, + const std::string& preferred_filament) { namespace fs = boost::filesystem; // Get current configuration from AppConfig - const auto old_vendors = app_config->vendors(); - const auto old_filaments = app_config->has_section(AppConfig::SECTION_FILAMENTS) - ? app_config->get_section(AppConfig::SECTION_FILAMENTS) - : std::map(); + const auto old_vendors = app_config->vendors(); + const auto old_filaments = app_config->has_section(AppConfig::SECTION_FILAMENTS) ? + app_config->get_section(AppConfig::SECTION_FILAMENTS) : + std::map(); // Find vendors that need installation std::vector install_bundles; - for (const auto &it : new_vendors) { + for (const auto& it : new_vendors) { if (it.second.size() > 0) { if (!is_vendor_installed(it.first)) { install_bundles.emplace_back(it.first); @@ -1281,9 +1294,9 @@ bool PresetBundle::apply_vendor_config( // override (e.g. replace "Generic ABS @System" with BBL "Generic ABS"). // When printers from the default bundle are also selected, keep @System // too since those printers need it. - static const std::string system_suffix = " @System"; - auto it_default = new_vendors.find(PresetBundle::ORCA_DEFAULT_BUNDLE); - bool has_default_bundle_printer = it_default != new_vendors.end() && !it_default->second.empty(); + static const std::string system_suffix = " @System"; + auto it_default = new_vendors.find(PresetBundle::ORCA_DEFAULT_BUNDLE); + bool has_default_bundle_printer = it_default != new_vendors.end() && !it_default->second.empty(); // Check if any non-default vendor has selected printers bool has_vendor_printer = false; @@ -1311,7 +1324,8 @@ bool PresetBundle::apply_vendor_config( if (vendor_it != this->vendors.end()) { // Vendor is loaded, check if the filament exists for (auto f : vendor_it->second.default_filaments) { - BOOST_LOG_TRIVIAL(info) << " checking if vendor filament " << f << " matches " << short_name << "(" << name << ")"; + BOOST_LOG_TRIVIAL(info) + << " checking if vendor filament " << f << " matches " << short_name << "(" << name << ")"; if (f.find(short_name) != std::string::npos) { BOOST_LOG_TRIVIAL(info) << name << " has filament from vendor: " << vendor; has_vendor_filament = true; @@ -1341,8 +1355,7 @@ bool PresetBundle::apply_vendor_config( if (overwrite) { app_config->set_section(AppConfig::SECTION_FILAMENTS, supplemented_filaments); app_config->set_vendors(new_vendors); - } - else { + } else { // Merge filaments std::map merged_filaments = old_filaments; for (const auto& [name, value] : supplemented_filaments) { @@ -1364,7 +1377,7 @@ bool PresetBundle::apply_vendor_config( // Load presets with new configuration this->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable, - {preferred_printer_model, preferred_printer_variant, preferred_filament, std::string()}); + {preferred_printer_model, preferred_printer_variant, preferred_filament, std::string()}); // Ensure active filament compatibility // If the active filament is not in the wizard-selected filaments, switch to the first @@ -1390,49 +1403,56 @@ bool PresetBundle::apply_vendor_config( } // Import presets from UI control -PresetsConfigSubstitutions PresetBundle::import_presets(std::vector & files, - std::function override_confirm, - ForwardCompatibilitySubstitutionRule rule, - AppConfig& config) +PresetsConfigSubstitutions PresetBundle::import_presets(std::vector& files, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + AppConfig& config) { bundles.PauseRead(); // Pause threads from reading BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " entry"; PresetsConfigSubstitutions substitutions; int overwrite = 0; - std::vector result; - std::string user_id = config.get("preset_folder"); + std::vector result; + std::string user_id = config.get("preset_folder"); if (user_id.empty()) user_id = DEFAULT_USER_FOLDER_NAME; this->update_user_presets_directory(user_id); - for (auto &file : files) { + for (auto& file : files) { if (Slic3r::is_json_file(file)) { import_json_presets(substitutions, file, override_confirm, rule, overwrite, result); } // Determine if it is a preset bundle - if (boost::iends_with(file, ".orca_printer") || boost::iends_with(file, ".orca_bundle") || boost::iends_with(file, ".orca_filament") || boost::iends_with(file, ".zip")) { + if (boost::iends_with(file, ".orca_printer") || boost::iends_with(file, ".orca_bundle") || + boost::iends_with(file, ".orca_filament") || boost::iends_with(file, ".zip")) { boost::system::error_code ec; // create user folder fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); - if (!fs::exists(user_folder)) fs::create_directory(user_folder, ec); - if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + if (!fs::exists(user_folder)) + fs::create_directory(user_folder, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); // create default folder fs::path configs_folder(user_folder / user_id); - if (!fs::exists(configs_folder)) fs::create_directory(configs_folder, ec); - if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); - //create temp folder - //std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp"; + if (!fs::exists(configs_folder)) + fs::create_directory(configs_folder, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + // create temp folder + // std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp"; fs::path temp_folder(configs_folder / "temp"); std::string user_default_temp_dir = temp_folder.make_preferred().string(); - if (fs::exists(temp_folder)) fs::remove_all(temp_folder); + if (fs::exists(temp_folder)) + fs::remove_all(temp_folder); fs::create_directory(temp_folder, ec); - if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + if (ec) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); file = boost::filesystem::path(file).make_preferred().string(); mz_zip_archive zip_archive; mz_zip_zero_struct(&zip_archive); mz_bool status; - FILE *zipFile = boost::nowide::fopen(file.c_str(), "rb"); + FILE* zipFile = boost::nowide::fopen(file.c_str(), "rb"); status = mz_zip_reader_init_cfile(&zip_archive, zipFile, 0, MZ_ZIP_FLAG_CASE_SENSITIVE | MZ_ZIP_FLAG_IGNORE_PATH); if (MZ_FALSE == status) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to initialize reader ZIP archive"; @@ -1446,17 +1466,19 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector bool has_bundle_structure = false; BundleMetadata metadata; fs::path metadata_path = temp_folder / BUNDLE_STRUCTURE_JSON_NAME; - status = mz_zip_reader_extract_file_to_file(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, encode_path(metadata_path.string().c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); + status = mz_zip_reader_extract_file_to_file(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, + encode_path(metadata_path.string().c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); if (status) { if (metadata.load_from_json(metadata_path.string())) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found bundle_id: " << metadata.id << " from " << BUNDLE_STRUCTURE_JSON_NAME; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found bundle_id: " << metadata.id << " from " + << BUNDLE_STRUCTURE_JSON_NAME; has_bundle_structure = true; } } if (has_bundle_structure && metadata.id.empty()) { boost::uuids::uuid uuid = boost::uuids::random_generator()(); - metadata.id = to_string(uuid); + metadata.id = to_string(uuid); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " bundle_id was empty, so generating a UUID: " << metadata.id; } @@ -1468,9 +1490,11 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector if (!fs::exists(bundle_base_dir)) fs::create_directories(bundle_base_dir, ec); if (ec) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to create bundle directory: " << bundle_base_dir.string() << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to create bundle directory: " << bundle_base_dir.string() + << " error: " << ec.message(); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No bundle_structure.json found, importing presets into the user preset directory"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << " No bundle_structure.json found, importing presets into the user preset directory"; } // Extract Files @@ -1485,18 +1509,21 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector if (std::string::npos != index) { file_name = file_name.substr(index + 1); } - if (BUNDLE_STRUCTURE_JSON_NAME == file_name) continue; + if (BUNDLE_STRUCTURE_JSON_NAME == file_name) + continue; // create target file path std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string(); - status = mz_zip_reader_extract_to_file(&zip_archive, i, encode_path(target_file_path.c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); + status = mz_zip_reader_extract_to_file(&zip_archive, i, encode_path(target_file_path.c_str()).c_str(), + MZ_ZIP_FLAG_CASE_SENSITIVE); // target file is opened if (MZ_FALSE == status) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to open target file: " << target_file_path; } else { bool is_success = import_json_presets(substitutions, target_file_path, override_confirm, rule, overwrite, result, has_bundle_structure ? bundle_base_dir.string() : std::string()); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success; + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success; } } } @@ -1514,9 +1541,8 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Saved bundle metadata to: " << metadata_save_path.string(); metadata.bundle_type = BundleType::Local; - metadata.path = metadata_save_path.string(); + metadata.path = metadata_save_path.string(); // Store the bundle metadata in m_bundles for tracking - bundles.WriteLock(); bundles.m_bundles[metadata.id] = metadata; @@ -1527,8 +1553,10 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector } fclose(zipFile); - if (fs::exists(temp_folder)) fs::remove_all(temp_folder, ec); - if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " remove directory failed: " << ec.message(); + if (fs::exists(temp_folder)) + fs::remove_all(temp_folder, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " remove directory failed: " << ec.message(); } } bundles.UnpauseRead(); @@ -1536,38 +1564,37 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector return substitutions; } -bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & substitutions, - std::string & file, - std::function override_confirm, - ForwardCompatibilitySubstitutionRule rule, - int & overwrite, - std::vector & result, - const std::string & bundle_dir) +bool PresetBundle::import_json_presets(PresetsConfigSubstitutions& substitutions, + std::string& file, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + int& overwrite, + std::vector& result, + const std::string& bundle_dir) { try { DynamicPrintConfig config; // BBS: change to json format // ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); std::map key_values; - std::string reason; - ConfigSubstitutions config_substitutions = config.load_from_json(file, rule, key_values, reason); - std::string name = key_values[BBL_JSON_KEY_NAME]; - std::string version_str = key_values[BBL_JSON_KEY_VERSION]; - boost::optional version = Semver::parse(version_str); - if (!version) return false; + std::string reason; + ConfigSubstitutions config_substitutions = config.load_from_json(file, rule, key_values, reason); + std::string name = key_values[BBL_JSON_KEY_NAME]; + std::string version_str = key_values[BBL_JSON_KEY_VERSION]; + boost::optional version = Semver::parse(version_str); + if (!version) + return false; - std::string type_subdir; // also note the type subdir for bundles - PresetCollection *collection = nullptr; + std::string type_subdir; // also note the type subdir for bundles + PresetCollection* collection = nullptr; if (config.has("printer_settings_id")) { - collection = &printers; + collection = &printers; type_subdir = PRESET_PRINTER_NAME; - } - else if (config.has("print_settings_id")) { - collection = &prints; + } else if (config.has("print_settings_id")) { + collection = &prints; type_subdir = PRESET_PRINT_NAME; - } - else if (config.has("filament_settings_id")) { - collection = &filaments; + } else if (config.has("filament_settings_id")) { + collection = &filaments; type_subdir = PRESET_FILAMENT_NAME; } if (collection == nullptr) { @@ -1575,15 +1602,17 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s return false; } const PresetOrigin load_origin = detect_origin_from_path(boost::filesystem::path(bundle_dir)); - const std::string preset_name = get_preset_canonical_name(name, load_origin); + const std::string preset_name = get_preset_canonical_name(name, load_origin); - if (overwrite == 0) overwrite = 1; + if (overwrite == 0) + overwrite = 1; if (auto p = collection->find_preset(preset_name, false)) { if (p->is_default || p->is_system) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present and is system preset, not loading: " << preset_name; return false; } - if (overwrite != 2 && overwrite != 3) overwrite = override_confirm(preset_name); //3: yes to all 2: no to all + if (overwrite != 2 && overwrite != 3) + overwrite = override_confirm(preset_name); // 3: yes to all 2: no to all } if (overwrite == 0 || overwrite == 2) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present, not loading: " << preset_name; @@ -1591,55 +1620,61 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s } DynamicPrintConfig new_config; - Preset * inherit_preset = nullptr; - ConfigOption * inherits_config = config.option(BBL_JSON_KEY_INHERITS); - std::string inherits_value; + Preset* inherit_preset = nullptr; + ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS); + std::string inherits_value; if (inherits_config) { - ConfigOptionString *option_str = dynamic_cast(inherits_config); + ConfigOptionString* option_str = dynamic_cast(inherits_config); inherits_value = option_str->value; inherit_preset = collection->find_preset2(inherits_value, true); Preset::normalize_inherits(config, inherit_preset); if (inherit_preset) - inherits_value = inherit_preset->name; // keep the base_id redo below in sync + inherits_value = inherit_preset->name; // keep the base_id redo below in sync } if (inherit_preset) { new_config = inherit_preset->config; new_config.apply(std::move(config)); } else { // We support custom root preset now - auto inherits_config2 = dynamic_cast(inherits_config); + auto inherits_config2 = dynamic_cast(inherits_config); if (inherits_config2 && !inherits_config2->value.empty()) { // we should skip this preset here - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find inherit preset for user preset %1%, just skip") % name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(", can not find inherit preset for user preset %1%, just skip") % name; return false; } - // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. - const Preset &default_preset = collection->default_preset_for(config); + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the + // "printer_technology" field. + const Preset& default_preset = collection->default_preset_for(config); new_config = default_preset.config; new_config.apply(std::move(config)); extend_default_config_length(new_config, true, default_preset.config); } - Preset &preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, std::move(new_config), false); + Preset& preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, + std::move(new_config), false); preset.bundle_id = load_origin.bundle_id; if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end()) preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID]; preset.is_external = true; preset.version = *version; inherit_preset = collection->find_preset(inherits_value, false, true); // pointer maybe wrong after insert, redo find - if (inherit_preset) preset.base_id = inherit_preset->setting_id; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " have filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; + if (inherit_preset) + preset.base_id = inherit_preset->setting_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " have filament_id: " << preset.filament_id + << " and base_id: " << preset.base_id; Preset::normalize(preset.config); // Report configuration fields, which are misplaced into a wrong group. - const Preset &default_preset = collection->default_preset_for(new_config); - std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); + const Preset& default_preset = collection->default_preset_for(new_config); + std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); if (!incorrect_keys.empty()) { ++m_errors; BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << preset.file << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; } if (!config_substitutions.empty()) - substitutions.push_back({name, collection->type(), PresetConfigSubstitutions::Source::UserFile, file, std::move(config_substitutions)}); + substitutions.push_back( + {name, collection->type(), PresetConfigSubstitutions::Source::UserFile, file, std::move(config_substitutions)}); collection->set_custom_preset_alias(preset); // If bundle_dir is provided, use it for the save operation @@ -1653,26 +1688,26 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s } result.push_back(file); - } catch (const std::ifstream::failure &err) { + } catch (const std::ifstream::failure& err) { ++m_errors; BOOST_LOG_TRIVIAL(error) << boost::format("The config cannot be loaded: %1%. Reason: %2%") % file % err.what(); - } catch (const std::runtime_error &err) { + } catch (const std::runtime_error& err) { ++m_errors; BOOST_LOG_TRIVIAL(error) << boost::format("Failed importing config file: %1%. Reason: %2%") % file % err.what(); } return true; } -//BBS save user preset to user_id preset folder +// BBS save user preset to user_id preset folder void PresetBundle::save_user_presets(AppConfig& config, std::map& need_to_delete_list) { std::string user_sub_folder = DEFAULT_USER_FOLDER_NAME; if (!config.get("preset_folder").empty()) user_sub_folder = config.get("preset_folder"); - //BBS: change directory by design - const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/"+ user_sub_folder; + // BBS: change directory by design + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user_sub_folder; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, save to %1%")%dir_user_presets; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, save to %1%") % dir_user_presets; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); if (!fs::exists(user_folder)) @@ -1704,7 +1739,7 @@ void PresetBundle::check_and_fix_user_presets_syncinfo(const std::string& user_i process_collection(this->printers); } -//Orca: Import subscribed bundle presets (load and save to disk in one operation) +// Orca: Import subscribed bundle presets (load and save to disk in one operation) PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( AppConfig& config, const std::map>& bundle_presets, @@ -1715,7 +1750,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( std::string errors_cumulative; bool process_added = false, filament_added = false, machine_added = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " enter, substitution_rule " << substitution_rule << ", bundle_id: " << remote_metadata.id << ", preset count: " << bundle_presets.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " enter, substitution_rule " << substitution_rule << ", bundle_id: " << remote_metadata.id + << ", preset count: " << bundle_presets.size(); BundleMetadata merged_metadata; auto existing_it = bundles.m_bundles.find(remote_metadata.id); @@ -1725,16 +1761,16 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( merged_metadata.imported_time = std::time(nullptr); } - merged_metadata.id = remote_metadata.id; - merged_metadata.name = remote_metadata.name; - merged_metadata.version = remote_metadata.version; - merged_metadata.description = remote_metadata.description; - merged_metadata.author = remote_metadata.author; - merged_metadata.updated_time = remote_metadata.updated_time; - merged_metadata.bundle_type = BundleType::Subscribed; - merged_metadata.is_subscribed = true; + merged_metadata.id = remote_metadata.id; + merged_metadata.name = remote_metadata.name; + merged_metadata.version = remote_metadata.version; + merged_metadata.description = remote_metadata.description; + merged_metadata.author = remote_metadata.author; + merged_metadata.updated_time = remote_metadata.updated_time; + merged_metadata.bundle_type = BundleType::Subscribed; + merged_metadata.is_subscribed = true; merged_metadata.update_available = false; - merged_metadata.unauthorized = false; + merged_metadata.unauthorized = false; const PresetOrigin subscribed_origin(PresetOrigin::Kind::SubscribedBundle, remote_metadata.id); @@ -1758,8 +1794,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( remote_printers.insert(subscribed_name); } - auto remove_obsolete_bundle_presets = - [&](PresetCollection& collection, const std::unordered_set& remote_names, const char* type_name) -> int { + auto remove_obsolete_bundle_presets = [&](PresetCollection& collection, const std::unordered_set& remote_names, + const char* type_name) -> int { int removed_count = 0; std::vector to_delete; @@ -1773,10 +1809,12 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( for (const std::string& preset_name : to_delete) { if (collection.delete_preset(preset_name, true)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << type_name << " preset '" << preset_name << "' no longer in remote bundle, deleted"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << type_name << " preset '" << preset_name + << "' no longer in remote bundle, deleted"; ++removed_count; } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to delete obsolete " << type_name << " preset '" << preset_name << "'"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to delete obsolete " << type_name << " preset '" << preset_name + << "'"; } } @@ -1794,7 +1832,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // Get current user ID for path construction std::string user_id = config.get("preset_folder"); - if (user_id.empty()) user_id = DEFAULT_USER_FOLDER_NAME; + if (user_id.empty()) + user_id = DEFAULT_USER_FOLDER_NAME; // Create the subscribed directory base path boost::filesystem::path user_folder(Slic3r::data_dir() + "/" + PRESET_USER_DIR); @@ -1805,7 +1844,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (!boost::filesystem::exists(subscribed_base)) boost::filesystem::create_directories(subscribed_base, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create subscribed directory: " << subscribed_base.string() << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create subscribed directory: " << subscribed_base.string() + << " error: " << ec.message(); return substitutions; } @@ -1816,7 +1856,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (!boost::filesystem::exists(bundle_dir)) boost::filesystem::create_directories(bundle_dir, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() + << " error: " << ec.message(); return substitutions; } @@ -1826,8 +1867,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // Load each preset from the bundle and save to disk for (const auto& preset_entry : bundle_presets) { - const std::string& preset_name = preset_entry.first; - const std::string subscribed_name = get_preset_canonical_name(preset_name, subscribed_origin); + const std::string& preset_name = preset_entry.first; + const std::string subscribed_name = get_preset_canonical_name(preset_name, subscribed_origin); std::map value_map = preset_entry.second; // Make a copy since we might modify it BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " importing preset: " << preset_name << " from bundle: " << remote_metadata.id; @@ -1842,7 +1883,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // If this preset inherits from another preset inside the same bundle, rewrite the // reference to the canonical (bundle-prefixed) name so the lookup matches the stored identity. auto inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); - if (inherits_iter != value_map.end() && !inherits_iter->second.empty() && bundle_presets.find(inherits_iter->second) != bundle_presets.end()) + if (inherits_iter != value_map.end() && !inherits_iter->second.empty() && + bundle_presets.find(inherits_iter->second) != bundle_presets.end()) inherits_iter->second = get_preset_canonical_name(inherits_iter->second, subscribed_origin); try { @@ -1852,23 +1894,23 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (type_iter->second == PRESET_IOT_PRINT_TYPE) { preset_collection = &(this->prints); - type_subdir = PRESET_PRINT_NAME; - preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); + type_subdir = PRESET_PRINT_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, + subscribed_origin); process_added |= preset_added; - } - else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { + } else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { preset_collection = &(this->filaments); - type_subdir = PRESET_FILAMENT_NAME; - preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); + type_subdir = PRESET_FILAMENT_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, + subscribed_origin); filament_added |= preset_added; - } - else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - type_subdir = PRESET_PRINTER_NAME; - preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); + type_subdir = PRESET_PRINTER_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, + subscribed_origin); machine_added |= preset_added; - } - else { + } else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid type " << type_iter->second << " for preset " << preset_name; continue; } @@ -1890,20 +1932,19 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) merged_metadata.printer_presets.push_back(preset->name); } - } - catch (const std::runtime_error& err) { + } catch (const std::runtime_error& err) { errors_cumulative += err.what(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " error importing preset " << preset_name << ": " << err.what(); } } boost::filesystem::path metadata_save_path = bundle_dir / PRESET_BUNDLE_METADATA; - merged_metadata.path = metadata_save_path.string(); - bundles.m_bundles[remote_metadata.id] = merged_metadata; + merged_metadata.path = metadata_save_path.string(); + bundles.m_bundles[remote_metadata.id] = merged_metadata; if (bundles.m_bundles[remote_metadata.id].save_to_json(metadata_save_path.string())) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved bundle metadata to: " << metadata_save_path.string(); - } else { + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to save bundle metadata to: " << metadata_save_path.string(); } @@ -1918,15 +1959,18 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (!errors_cumulative.empty()) throw Slic3r::RuntimeError(errors_cumulative); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " finished, process_added " << process_added << ", filament_added " << filament_added << ", machine_added " << machine_added; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " finished, process_added " << process_added << ", filament_added " << filament_added + << ", machine_added " << machine_added; return substitutions; } // Helper function: save preset to bundle directory with common logic // This function extracts the common code used by both import_json_presets and import_subscribed_presets -bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection, - const std::string& bundle_id, const std::string& type_subdir, - const std::string& bundle_base_dir) +bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, + PresetCollection* collection, + const std::string& bundle_id, + const std::string& type_subdir, + const std::string& bundle_base_dir) { if (bundle_base_dir.empty()) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " bundle_base_dir is empty, cannot save preset " << preset.name; @@ -1943,7 +1987,8 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* c if (!boost::filesystem::exists(bundle_dir)) boost::filesystem::create_directories(bundle_dir, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() + << " error: " << ec.message(); return false; } @@ -1952,7 +1997,8 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* c if (!boost::filesystem::exists(type_dir)) { boost::filesystem::create_directories(type_dir, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create type directory: " << type_dir.string() << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create type directory: " << type_dir.string() + << " error: " << ec.message(); return false; } } @@ -1962,8 +2008,8 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* c // Bundle preset names may include the subscribed/local prefix path. // Persist the file under the type directory using only the base preset name. const std::string preset_filename = boost::filesystem::path(preset.name).filename().string(); - const std::string file_name = boost::iends_with(preset_filename, ".json") ? preset_filename : (preset_filename + ".json"); - preset.file = (type_dir / file_name).make_preferred().string(); + const std::string file_name = boost::iends_with(preset_filename, ".json") ? preset_filename : (preset_filename + ".json"); + preset.file = (type_dir / file_name).make_preferred().string(); // Save with parent config if inherits from another preset std::string inherits = Preset::inherits(preset.config); @@ -1979,10 +2025,8 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* c Preset::normalize_inherits(preset.config, parent_preset); if (preset.base_id.empty()) preset.base_id = parent_preset->setting_id; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved preset " << preset.name - << " filament_id: " << preset.filament_id - << " base_id: " << preset.base_id - << " bundle: " << bundle_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved preset " << preset.name << " filament_id: " << preset.filament_id + << " base_id: " << preset.base_id << " bundle: " << bundle_id; preset.save(&(parent_preset->config)); } } @@ -1998,13 +2042,13 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* c } } -//BBS: save user preset to user_id preset folder +// BBS: save user preset to user_id preset folder void PresetBundle::update_user_presets_directory(const std::string preset_folder) { - //BBS: change directory by design - const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/"+ preset_folder; + // BBS: change directory by design + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + preset_folder; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, update directory to %1%")%dir_user_presets; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, update directory to %1%") % dir_user_presets; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); if (!fs::exists(user_folder)) @@ -2025,7 +2069,8 @@ void PresetBundle::remove_user_presets_directory(const std::string preset_folder const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + preset_folder; if (preset_folder.empty()) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": preset_folder is empty, no need to remove directory : %1%") % dir_user_presets; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(": preset_folder is empty, no need to remove directory : %1%") % dir_user_presets; return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, delete directory : %1%") % dir_user_presets; @@ -2037,72 +2082,69 @@ void PresetBundle::remove_user_presets_directory(const std::string preset_folder void PresetBundle::update_system_preset_setting_ids(std::map>& system_presets) { - for (auto iterator: system_presets) - { - std::string name = iterator.first; + for (auto iterator : system_presets) { + std::string name = iterator.first; std::map& value_map = iterator.second; - //get the type first + // get the type first std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); if (type_iter == value_map.end()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%")%name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%") % name; continue; } - PresetCollection *preset_collection = nullptr; + PresetCollection* preset_collection = nullptr; if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - } - else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - } - else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - } - else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") %type_iter->second %name; + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") % type_iter->second % name; continue; } std::string setting_id; if (value_map.count(BBL_JSON_KEY_SETTING_ID) > 0) setting_id = value_map[BBL_JSON_KEY_SETTING_ID]; else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find setting_id for setting %1%")%name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find setting_id for setting %1%") % name; continue; } Preset* preset = preset_collection->find_preset(name, false, true); if (preset) { if (!preset->setting_id.empty() && (preset->setting_id.compare(setting_id) != 0)) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << boost::format("name %1%, local setting_id %2% is different with remote id %3%") - %preset->name %preset->setting_id %setting_id; - } - else if (preset->setting_id.empty()) + BOOST_LOG_TRIVIAL(error) << boost::format("name %1%, local setting_id %2% is different with remote id %3%") % preset->name % + preset->setting_id % setting_id; + } else if (preset->setting_id.empty()) preset->setting_id = setting_id; - } - else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find setting %1% in system presets, type %2%") %name %type_iter->second; + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format("can not find setting %1% in system presets, type %2%") % name % type_iter->second; continue; } } return; } -//BBS: validate printers from previous project -static std::set gcodes_key_set = {"filament_end_gcode", "filament_start_gcode", "change_filament_gcode", "layer_change_gcode", "machine_end_gcode", "machine_pause_gcode", "machine_start_gcode", - "template_custom_gcode", "printing_by_object_gcode", "before_layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode"}; -int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintConfig& config, std::set& different_gcodes) +// BBS: validate printers from previous project +static std::set gcodes_key_set = {"filament_end_gcode", "filament_start_gcode", "change_filament_gcode", + "layer_change_gcode", "machine_end_gcode", "machine_pause_gcode", + "machine_start_gcode", "template_custom_gcode", "printing_by_object_gcode", + "before_layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode"}; +int PresetBundle::validate_presets(const std::string& file_name, DynamicPrintConfig& config, std::set& different_gcodes) { - bool validated = false; - std::vector inherits_values = config.option("inherits_group", true)->values; - std::vector filament_preset_name = config.option("filament_settings_id", true)->values; - std::string printer_preset = config.option("printer_settings_id", true)->value; - bool has_different_settings_to_system = config.option("different_settings_to_system")?true:false; + bool validated = false; + std::vector inherits_values = config.option("inherits_group", true)->values; + std::vector filament_preset_name = config.option("filament_settings_id", true)->values; + std::string printer_preset = config.option("printer_settings_id", true)->value; + bool has_different_settings_to_system = config.option("different_settings_to_system") ? true : false; std::vector different_values; - int ret = VALIDATE_PRESETS_SUCCESS; + int ret = VALIDATE_PRESETS_SUCCESS; if (has_different_settings_to_system) different_values = config.option("different_settings_to_system", true)->values; - //PrinterTechnology printer_technology = Preset::printer_technology(config); + // PrinterTechnology printer_technology = Preset::printer_technology(config); size_t filament_count = config.option("filament_diameter")->values.size(); inherits_values.resize(filament_count + 2, std::string()); different_values.resize(filament_count + 2, std::string()); @@ -2112,14 +2154,14 @@ int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintCon validated = this->printers.validate_preset(printer_preset, printer_inherits); if (!validated) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found the printer preset not inherit from system") % file_name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(":file_name %1%, found the printer preset not inherit from system") % file_name; different_gcodes.emplace(printer_preset); ret = VALIDATE_PRESETS_PRINTER_NOT_FOUND; } - for(unsigned int index = 0; index < filament_count; index ++) - { - std::string filament_preset = filament_preset_name[index]; - std::string filament_inherits = inherits_values[index+1]; + for (unsigned int index = 0; index < filament_count; index++) { + std::string filament_preset = filament_preset_name[index]; + std::string filament_inherits = inherits_values[index + 1]; // filament_preset_name is padded up to filament_count from filament_diameter. Unfilled // slots have no assigned preset, so there's nothing to validate or warn about. @@ -2128,21 +2170,23 @@ int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintCon validated = this->filaments.validate_preset(filament_preset, filament_inherits); if (!validated) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found the filament %2% preset not inherit from system") % file_name %(index+1); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(":file_name %1%, found the filament %2% preset not inherit from system") % + file_name % (index + 1); different_gcodes.emplace(filament_preset); ret = VALIDATE_PRESETS_FILAMENTS_NOT_FOUND; } } - //self defined presets, return directly - if (ret) - { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found self defined presets, count %2%") %file_name %different_gcodes.size(); + // self defined presets, return directly + if (ret) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(":file_name %1%, found self defined presets, count %2%") % file_name % + different_gcodes.size(); return ret; } - for(unsigned int index = 1; index < filament_count; index ++) - { + for (unsigned int index = 1; index < filament_count; index++) { std::string different_settingss = different_values[index]; std::vector different_keys; @@ -2152,14 +2196,16 @@ int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintCon for (unsigned int j = 0; j < different_keys.size(); j++) { if (gcodes_key_set.find(different_keys[j]) != gcodes_key_set.end()) { different_gcodes.emplace(different_keys[j]); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":preset index %1%, different key %2%") %index %different_keys[j]; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(":preset index %1%, different key %2%") % index % different_keys[j]; } } } - if (!different_gcodes.empty()) - { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found different gcodes count %2%") %file_name %different_gcodes.size(); + if (!different_gcodes.empty()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(":file_name %1%, found different gcodes count %2%") % file_name % + different_gcodes.size(); return VALIDATE_PRESETS_MODIFIED_GCODES; } @@ -2168,13 +2214,17 @@ int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintCon return VALIDATE_PRESETS_SUCCESS; } -void PresetBundle::remove_users_preset(AppConfig &config, std::map> *my_presets) +void PresetBundle::remove_users_preset(AppConfig& config, std::map>* my_presets) { - auto check_removed = [my_presets, this](Preset &preset) -> bool { - if (my_presets == nullptr) return true; - if (my_presets->find(preset.name) != my_presets->end()) return false; - if (!preset.sync_info.empty()) return false; // syncing, not remove - if (preset.setting_id.empty()) return false; // no id, not remove + auto check_removed = [my_presets, this](Preset& preset) -> bool { + if (my_presets == nullptr) + return true; + if (my_presets->find(preset.name) != my_presets->end()) + return false; + if (!preset.sync_info.empty()) + return false; // syncing, not remove + if (preset.setting_id.empty()) + return false; // no id, not remove // Saved preset is removed by another session if (preset.is_dirty) { preset.setting_id.clear(); @@ -2183,17 +2233,18 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::mapis_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":printers erase %1%, type %2%, user_id %3%") % it->name % Preset::get_type_string(it->type) % it->user_id; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(":printers erase %1%, type %2%, user_id %3%") % it->name % + Preset::get_type_string(it->type) % it->user_id; if (it->name == printer_selected_preset_name) need_reset_printer_preset = true; it = printers.erase(it); - } - else { + } else { it++; } } @@ -2215,16 +2266,17 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::mapis_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":prints erase %1%, type %2%, user_id %3%")%it->name %Preset::get_type_string(it->type) %it->user_id; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(":prints erase %1%, type %2%, user_id %3%") % it->name % + Preset::get_type_string(it->type) % it->user_id; if (it->name == selected_print_name) need_reset_print_preset = true; it = prints.erase(it); - } - else { + } else { it++; } } @@ -2236,20 +2288,22 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::mapis_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":filaments erase %1%, type %2%, user_id %3%")%it->name %Preset::get_type_string(it->type) %it->user_id; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(":filaments erase %1%, type %2%, user_id %3%") % it->name % + Preset::get_type_string(it->type) % it->user_id; if (it->name == selected_filament_name) need_reset_filament_preset = true; it = filaments.erase(it); - } - else { + } else { it++; } } if (need_reset_filament_preset && printers.get_selected_preset().config.has("default_filament_profile")) { - const std::vector& prefered_filament_profiles = printers.get_selected_preset().config.option("default_filament_profile")->values; + const std::vector& prefered_filament_profiles = + printers.get_selected_preset().config.option("default_filament_profile")->values; if (prefered_filament_profiles.size() > 0) filaments.select_preset_by_name(prefered_filament_profiles[0], true); } else { @@ -2259,8 +2313,7 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::mapfilaments.find_preset(filament_presets[i]); if (preset == nullptr) filament_presets[i] = filaments.get_selected_preset_name(); @@ -2278,11 +2331,12 @@ void PresetBundle::clear_printer_hold_aliases() this->printers.m_printer_hold_alias.clear(); } -//BBS: add json related logic, load system presets from json -std::pair PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) +// BBS: add json related logic, load system presets from json +std::pair PresetBundle::load_system_presets_from_json( + ForwardCompatibilitySubstitutionRule compatibility_rule) { - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) // Loading system presets, don't log substitutions. compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; @@ -2291,8 +2345,8 @@ std::pair PresetBundle::load_system_pre compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; // Here the vendor specific read only Config Bundles are stored. - //BBS: change directory by design - boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + // BBS: change directory by design + boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); @@ -2301,10 +2355,10 @@ std::pair PresetBundle::load_system_pre // The vendors below are loaded whole and against each other — the filament // library first, then every other vendor with it as the base — so each parse // is complete enough to be worth caching. - m_generate_vendor_caches = m_generate_vendor_caches || ! validation_mode; + m_generate_vendor_caches = m_generate_vendor_caches || !validation_mode; - PresetsConfigSubstitutions substitutions; - std::string errors_cummulative; + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; bool first = true; // Sorted, so any duplicate-preset warning below comes out in the same order on // every run. @@ -2324,16 +2378,17 @@ std::pair PresetBundle::load_system_pre } // Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously. - if (! orca_lib_vendor.empty()) { + if (!orca_lib_vendor.empty()) { try { // Match a fresh launch before parsing: hold aliases and the error // counter survive reset(), and would otherwise carry prior-cycle // state into this load. this->clear_printer_hold_aliases(); this->m_errors = 0; - append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); + append(substitutions, + this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); first = false; - } catch (const std::runtime_error &err) { + } catch (const std::runtime_error& err) { if (validation_mode) throw err; errors_cummulative += err.what(); @@ -2344,26 +2399,25 @@ std::pair PresetBundle::load_system_pre // Step 2: Load remaining vendors in parallel. Each gets its own // PresetBundle and uses `this` (which contains ORCA_FILAMENT_LIBRARY) // as the base_bundle for cross-bundle inheritance lookups. - std::vector> parallel_bundles(other_vendors.size()); - std::vector parallel_substitutions(other_vendors.size()); - std::vector parallel_errors(other_vendors.size()); + std::vector> parallel_bundles(other_vendors.size()); + std::vector parallel_substitutions(other_vendors.size()); + std::vector parallel_errors(other_vendors.size()); - tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), - [&](const tbb::blocked_range& range) { - for (size_t i = range.begin(); i < range.end(); ++i) { - auto bundle = std::make_unique(); - bundle->set_is_validation_mode(validation_mode); - bundle->set_generate_vendor_caches(m_generate_vendor_caches); - try { - auto result = bundle->load_vendor_configs_from_json( - dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this); - parallel_substitutions[i] = std::move(result.first); - parallel_bundles[i] = std::move(bundle); - } catch (const std::runtime_error &err) { - parallel_errors[i] = err.what(); - } + tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), [&](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + auto bundle = std::make_unique(); + bundle->set_is_validation_mode(validation_mode); + bundle->set_generate_vendor_caches(m_generate_vendor_caches); + try { + auto result = bundle->load_vendor_configs_from_json(dir.string(), other_vendors[i], PresetBundle::LoadSystem, + compatibility_rule, this); + parallel_substitutions[i] = std::move(result.first); + parallel_bundles[i] = std::move(bundle); + } catch (const std::runtime_error& err) { + parallel_errors[i] = err.what(); } - }); + } + }); // Step 3: Sequentially merge the parallel-loaded bundles into `this`. // The merge order is the original vendor order so any duplicate-warning @@ -2382,7 +2436,7 @@ std::pair PresetBundle::load_system_pre const std::string& vendor_name = other_vendors[i]; append(substitutions, std::move(parallel_substitutions[i])); std::vector duplicates = this->merge_presets(std::move(*parallel_bundles[i])); - first = false; + first = false; if (!duplicates.empty()) { errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; for (size_t j = 0; j < duplicates.size(); ++j) { @@ -2396,22 +2450,22 @@ std::pair PresetBundle::load_system_pre } if (first) { - // No config bundle loaded, reset. - this->reset(false); - } + // No config bundle loaded, reset. + this->reset(false); + } - this->update_system_maps(); + this->update_system_maps(); - const auto load_ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - load_t0).count(); + const auto load_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - load_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << vendor_names.size() << " vendor(s) loaded in " << load_ms << " ms"; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%") % errors_cummulative; return std::make_pair(std::move(substitutions), errors_cummulative); } -std::pair PresetBundle::load_system_models_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) +std::pair PresetBundle::load_system_models_from_json( + ForwardCompatibilitySubstitutionRule compatibility_rule) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) @@ -2422,10 +2476,10 @@ std::pair PresetBundle::load_system_mod compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; // Here the vendor specific read only Config Bundles are stored. - boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); PresetsConfigSubstitutions substitutions; - std::string errors_cummulative; - for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string errors_cummulative; + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { std::string vendor_file = dir_entry.path().string(); if (Slic3r::is_json_file(vendor_file)) { std::string vendor_name = dir_entry.path().filename().string(); @@ -2433,8 +2487,9 @@ std::pair PresetBundle::load_system_mod vendor_name.erase(vendor_name.size() - 5); try { // Load the config bundle, flatten it. - append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); - } catch (const std::runtime_error &err) { + append(substitutions, + load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); errors_cummulative += "\n"; } @@ -2445,7 +2500,8 @@ std::pair PresetBundle::load_system_mod return std::make_pair(std::move(substitutions), errors_cummulative); } -std::pair PresetBundle::load_system_filaments_json(ForwardCompatibilitySubstitutionRule compatibility_rule) +std::pair PresetBundle::load_system_filaments_json( + ForwardCompatibilitySubstitutionRule compatibility_rule) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) @@ -2456,11 +2512,11 @@ std::pair PresetBundle::load_system_fil compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; // Here the vendor specific read only Config Bundles are stored. - boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); PresetsConfigSubstitutions substitutions; - std::string errors_cummulative; - bool first = true; - for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string errors_cummulative; + bool first = true; + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { std::string vendor_file = dir_entry.path().string(); if (Slic3r::is_json_file(vendor_file)) { std::string vendor_name = dir_entry.path().filename().string(); @@ -2469,23 +2525,31 @@ std::pair PresetBundle::load_system_fil try { if (first) { // Reset this PresetBundle and load the first vendor config. - append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); + append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, + PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, + compatibility_rule) + .first); first = false; } else { // Load the other vendor configs, merge them with this PresetBundle. // Report duplicate profiles. PresetBundle other; - append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); + append(substitutions, + other + .load_vendor_configs_from_json(dir.string(), vendor_name, + PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule) + .first); std::vector duplicates = this->merge_presets(std::move(other)); if (!duplicates.empty()) { errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; for (size_t i = 0; i < duplicates.size(); ++i) { - if (i > 0) errors_cummulative += ", "; + if (i > 0) + errors_cummulative += ", "; errors_cummulative += duplicates[i]; } } } - } catch (const std::runtime_error &err) { + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); errors_cummulative += "\n"; } @@ -2501,17 +2565,19 @@ VendorProfile PresetBundle::get_custom_vendor_models() const VendorProfile vendor; vendor.name = PRESET_CUSTOM_VENDOR; vendor.id = PRESET_CUSTOM_VENDOR; - for (auto &preset : printers.get_presets()) { - if (preset.is_system) continue; - if (printers.get_preset_base(preset) != &preset) continue; - if (preset.is_default) continue; - auto model = preset.config.opt_string("printer_model"); - auto variant = preset.config.opt_string("printer_variant"); - auto iter_model = std::find_if(vendor.models.begin(), vendor.models.end(), [model](VendorProfile::PrinterModel &m) { - return m.name == model; - }); + for (auto& preset : printers.get_presets()) { + if (preset.is_system) + continue; + if (printers.get_preset_base(preset) != &preset) + continue; + if (preset.is_default) + continue; + auto model = preset.config.opt_string("printer_model"); + auto variant = preset.config.opt_string("printer_variant"); + auto iter_model = std::find_if(vendor.models.begin(), vendor.models.end(), + [model](VendorProfile::PrinterModel& m) { return m.name == model; }); if (iter_model == vendor.models.end()) { - iter_model = vendor.models.emplace(vendor.models.end(), VendorProfile::PrinterModel{}); + iter_model = vendor.models.emplace(vendor.models.end(), VendorProfile::PrinterModel{}); iter_model->id = model; iter_model->name = model; iter_model->variants = {VendorProfile::PrinterVariant(variant)}; @@ -2523,21 +2589,21 @@ VendorProfile PresetBundle::get_custom_vendor_models() const } // Merge one vendor's presets with the other vendor's presets, report duplicates. -std::vector PresetBundle::merge_presets(PresetBundle &&other) +std::vector PresetBundle::merge_presets(PresetBundle&& other) { this->vendors.insert(other.vendors.begin(), other.vendors.end()); - std::vector duplicate_prints = this->prints .merge_presets(std::move(other.prints), this->vendors); - std::vector duplicate_sla_prints = this->sla_prints .merge_presets(std::move(other.sla_prints), this->vendors); - std::vector duplicate_filaments = this->filaments .merge_presets(std::move(other.filaments), this->vendors); + std::vector duplicate_prints = this->prints.merge_presets(std::move(other.prints), this->vendors); + std::vector duplicate_sla_prints = this->sla_prints.merge_presets(std::move(other.sla_prints), this->vendors); + std::vector duplicate_filaments = this->filaments.merge_presets(std::move(other.filaments), this->vendors); std::vector duplicate_sla_materials = this->sla_materials.merge_presets(std::move(other.sla_materials), this->vendors); - std::vector duplicate_printers = this->printers .merge_presets(std::move(other.printers), this->vendors); - append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints)); - append(this->obsolete_presets.sla_prints, std::move(other.obsolete_presets.sla_prints)); - append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments)); + std::vector duplicate_printers = this->printers.merge_presets(std::move(other.printers), this->vendors); + append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints)); + append(this->obsolete_presets.sla_prints, std::move(other.obsolete_presets.sla_prints)); + append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments)); append(this->obsolete_presets.sla_materials, std::move(other.obsolete_presets.sla_materials)); - append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers)); - append(duplicate_prints, std::move(duplicate_sla_prints)); - append(duplicate_prints, std::move(duplicate_filaments)); + append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers)); + append(duplicate_prints, std::move(duplicate_sla_prints)); + append(duplicate_prints, std::move(duplicate_filaments)); append(duplicate_prints, std::move(duplicate_sla_materials)); append(duplicate_prints, std::move(duplicate_printers)); m_errors += other.m_errors; @@ -2546,22 +2612,22 @@ std::vector PresetBundle::merge_presets(PresetBundle &&other) void PresetBundle::update_system_maps() { - this->prints .update_map_system_profile_renamed(); - this->sla_prints .update_map_system_profile_renamed(); - this->filaments .update_map_system_profile_renamed(); + this->prints.update_map_system_profile_renamed(); + this->sla_prints.update_map_system_profile_renamed(); + this->filaments.update_map_system_profile_renamed(); this->sla_materials.update_map_system_profile_renamed(); - this->printers .update_map_system_profile_renamed(); + this->printers.update_map_system_profile_renamed(); - this->prints .update_map_alias_to_profile_name(); - this->sla_prints .update_map_alias_to_profile_name(); - this->filaments .update_map_alias_to_profile_name(); + this->prints.update_map_alias_to_profile_name(); + this->sla_prints.update_map_alias_to_profile_name(); + this->filaments.update_map_alias_to_profile_name(); this->sla_materials.update_map_alias_to_profile_name(); - this->printers .update_map_alias_to_profile_name(); + this->printers.update_map_alias_to_profile_name(); this->filaments.update_library_profile_excluded_from(); } -static inline std::string remove_ini_suffix(const std::string &name) +static inline std::string remove_ini_suffix(const std::string& name) { std::string out = name; if (boost::iends_with(out, ".ini")) @@ -2572,14 +2638,14 @@ static inline std::string remove_ini_suffix(const std::string &name) // Set the "enabled" flag for printer vendors, printer models and printer variants // based on the user configuration. // If the "vendor" section is missing, enable all models and variants of the particular vendor. -void PresetBundle::load_installed_printers(const AppConfig &config) +void PresetBundle::load_installed_printers(const AppConfig& config) { - this->update_system_maps(); - for (auto &preset : printers) + this->update_system_maps(); + for (auto& preset : printers) preset.set_visible_from_appconfig(config); } -const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& preset_type, const std::string& alias) const +const std::string& PresetBundle::get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias) const { if (preset_type == Preset::TYPE_INVALID) return alias; @@ -2588,21 +2654,21 @@ const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& p preset_type == Preset::TYPE_SLA_PRINT ? sla_prints : preset_type == Preset::TYPE_FILAMENT ? filaments : preset_type == Preset::TYPE_PRINTER ? printers : - sla_materials; + sla_materials; return presets.get_preset_name_by_alias(alias); } -//BBS: get filament required hrc by filament type +// BBS: get filament required hrc by filament type const int PresetBundle::get_required_hrc_by_filament_type(const std::string& filament_type) const { - static std::unordered_mapfilament_type_to_hrc; + static std::unordered_map filament_type_to_hrc; if (filament_type_to_hrc.empty()) { for (auto iter = filaments.m_presets.begin(); iter != filaments.m_presets.end(); iter++) { if (iter->vendor && iter->vendor->id == "BBL") { if (iter->config.has("filament_type") && iter->config.has("required_nozzle_HRC")) { - auto type = iter->config.opt_string("filament_type", 0); - auto hrc = iter->config.opt_int("required_nozzle_HRC", 0); + auto type = iter->config.opt_string("filament_type", 0); + auto hrc = iter->config.opt_int("required_nozzle_HRC", 0); filament_type_to_hrc[type] = hrc; } } @@ -2615,14 +2681,17 @@ const int PresetBundle::get_required_hrc_by_filament_type(const std::string& fil return 0; } -//BBS: add project embedded preset logic -void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset::Type type, - const std::vector& unselected_options, bool save_to_project) +// BBS: add project embedded preset logic +void PresetBundle::save_changes_for_preset(const std::string& new_name, + Preset::Type type, + const std::vector& unselected_options, + bool save_to_project) { - PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : - type == Preset::TYPE_SLA_PRINT ? sla_prints : - type == Preset::TYPE_FILAMENT ? filaments : - type == Preset::TYPE_SLA_MATERIAL ? sla_materials : printers; + PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : + type == Preset::TYPE_SLA_PRINT ? sla_prints : + type == Preset::TYPE_FILAMENT ? filaments : + type == Preset::TYPE_SLA_MATERIAL ? sla_materials : + printers; // if we want to save just some from selected options if (!unselected_options.empty()) { @@ -2631,11 +2700,12 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset:: } // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini - //BBS: add project embedded preset logic - //presets.save_current_preset(new_name); + // BBS: add project embedded preset logic + // presets.save_current_preset(new_name); presets.save_current_preset(new_name, false, save_to_project); // Mark the print & filament enabled if they are compatible with the currently selected preset. - // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. + // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a + // "red flag" icon showing that they are no more compatible. update_compatible(PresetSelectCompatibleType::Never); if (type == Preset::TYPE_FILAMENT) { @@ -2644,96 +2714,172 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset:: } } -void PresetBundle::load_installed_filaments(AppConfig &config) +void PresetBundle::load_installed_filaments(AppConfig& config) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, printer size %1%")%printers.size(); - //if (! config.has_section(AppConfig::SECTION_FILAMENTS) - // || config.get_section(AppConfig::SECTION_FILAMENTS).empty()) { - // Compatibility with the PrusaSlicer 2.1.1 and older, where the filament profiles were not installable yet. - // Find all filament profiles, which are compatible with installed printers, and act as if these filament profiles - // were installed. - std::unordered_set compatible_filaments; - for (const Preset &printer : printers) - if (printer.is_visible && printer.printer_technology() == ptFFF && printer.vendor && (!printer.vendor->models.empty())) { - bool add_default_materials = true; - if (config.has_section(AppConfig::SECTION_FILAMENTS)) - { - const std::map& installed_filament = config.get_section(AppConfig::SECTION_FILAMENTS); - for (auto filament_iter : installed_filament) - { - Preset* filament = filaments.find_preset(filament_iter.first, false, true); - if (filament && is_compatible_with_printer(PresetWithVendorProfile(*filament, filament->vendor), PresetWithVendorProfile(printer, printer.vendor))) - { - - //already has compatible filament - add_default_materials = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1% vendor %2% already has default filament %3%")%printer.name %printer.vendor %filament_iter.first; - break; - } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, printer size %1%") % printers.size(); + // if (! config.has_section(AppConfig::SECTION_FILAMENTS) + // || config.get_section(AppConfig::SECTION_FILAMENTS).empty()) { + // Compatibility with the PrusaSlicer 2.1.1 and older, where the filament profiles were not installable yet. + // Find all filament profiles, which are compatible with installed printers, and act as if these filament profiles + // were installed. + std::unordered_set compatible_filaments; + for (const Preset& printer : printers) + if (printer.is_visible && printer.printer_technology() == ptFFF && printer.vendor && (!printer.vendor->models.empty())) { + bool add_default_materials = true; + if (config.has_section(AppConfig::SECTION_FILAMENTS)) { + const std::map& installed_filament = config.get_section(AppConfig::SECTION_FILAMENTS); + for (auto filament_iter : installed_filament) { + Preset* filament = filaments.find_preset(filament_iter.first, false, true); + if (filament && is_compatible_with_printer(PresetWithVendorProfile(*filament, filament->vendor), + PresetWithVendorProfile(printer, printer.vendor))) { + // already has compatible filament + add_default_materials = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": printer %1% vendor %2% already has default filament %3%") % + printer.name % printer.vendor % filament_iter.first; + break; } } - - if (!add_default_materials) - continue; - - const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); - if (!printer_model) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find printer_model for printer %1%")%printer.name; - continue; - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1% vendor %2% don't have filament visible, will add %3% default filaments")%printer.name %printer.vendor %printer_model->default_materials.size(); - for (auto default_filament: printer_model->default_materials) - { - Preset* filament = filaments.find_preset(default_filament, false, true); - if (filament && filament->is_system) - compatible_filaments.insert(filament); - } - //const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); - //for (const Preset &filament : filaments) - // if (filament.is_system && is_compatible_with_printer(filaments.get_preset_with_vendor_profile(filament), printer_with_vendor_profile)) - // compatible_filaments.insert(&filament); } - // and mark these filaments as installed, therefore this code will not be executed at the next start of the application. - for (const auto &filament: compatible_filaments) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set filament %1% to visible by default")%filament->name; - config.set(AppConfig::SECTION_FILAMENTS, filament->name, "true"); + + if (!add_default_materials) + continue; + + const VendorProfile::PrinterModel* printer_model = PresetUtils::system_printer_model(printer); + if (!printer_model) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find printer_model for printer %1%") % printer.name; + continue; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format( + ": printer %1% vendor %2% don't have filament visible, will add %3% default filaments") % + printer.name % printer.vendor % printer_model->default_materials.size(); + for (auto default_filament : printer_model->default_materials) { + Preset* filament = filaments.find_preset(default_filament, false, true); + if (filament && filament->is_system) + compatible_filaments.insert(filament); + } + // const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + // for (const Preset &filament : filaments) + // if (filament.is_system && is_compatible_with_printer(filaments.get_preset_with_vendor_profile(filament), + // printer_with_vendor_profile)) + // compatible_filaments.insert(&filament); } + // and mark these filaments as installed, therefore this code will not be executed at the next start of the application. + for (const auto& filament : compatible_filaments) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set filament %1% to visible by default") % filament->name; + config.set(AppConfig::SECTION_FILAMENTS, filament->name, "true"); + } //} - for (auto &preset : filaments) + for (auto& preset : filaments) preset.set_visible_from_appconfig(config); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": exit."); } -void PresetBundle::load_installed_sla_materials(AppConfig &config) +void PresetBundle::load_installed_sla_materials(AppConfig& config) { - if (! config.has_section(AppConfig::SECTION_MATERIALS)) { + if (!config.has_section(AppConfig::SECTION_MATERIALS)) { std::unordered_set comp_sla_materials; - // Compatibility with the PrusaSlicer 2.1.1 and older, where the SLA material profiles were not installable yet. - // Find all SLA material profiles, which are compatible with installed printers, and act as if these SLA material profiles - // were installed. - for (const Preset &printer : printers) + // Compatibility with the PrusaSlicer 2.1.1 and older, where the SLA material profiles were not installable yet. + // Find all SLA material profiles, which are compatible with installed printers, and act as if these SLA material profiles + // were installed. + for (const Preset& printer : printers) if (printer.is_visible && printer.printer_technology() == ptSLA) { - const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); - for (const Preset &material : sla_materials) - if (material.is_system && is_compatible_with_printer(sla_materials.get_preset_with_vendor_profile(material), printer_with_vendor_profile)) - comp_sla_materials.insert(&material); - } - // and mark these SLA materials as installed, therefore this code will not be executed at the next start of the application. - for (const auto &material: comp_sla_materials) + const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + for (const Preset& material : sla_materials) + if (material.is_system && + is_compatible_with_printer(sla_materials.get_preset_with_vendor_profile(material), printer_with_vendor_profile)) + comp_sla_materials.insert(&material); + } + // and mark these SLA materials as installed, therefore this code will not be executed at the next start of the application. + for (const auto& material : comp_sla_materials) config.set(AppConfig::SECTION_MATERIALS, material->name, "true"); } - for (auto &preset : sla_materials) + for (auto& preset : sla_materials) preset.set_visible_from_appconfig(config); } -void PresetBundle::update_selections(AppConfig &config) +// Mixed-color filament metadata is project state saved in the 3mf, also mirrored into the app +// config so the last session's mixes are back before any project is opened. It is kept in the +// per-printer snapshot next to the filament list it indexes (filament_%02u/filament_colors), +// because that list is rebuilt on every printer selection and the component ids are 1-based +// indices into exactly that list. Missing keys clear the arrays, so one printer never inherits +// another's mixes; fallback_to_global also reads the shared "presets" keys an older config +// layout used, which export_selections drops on the next save. +static void load_mixed_filament_settings( + DynamicPrintConfig& project_config, AppConfig& config, const std::string& printer_name, size_t n_filaments, bool fallback_to_global) { - std::string initial_printer_profile_name = printers.get_selected_preset_name(); + auto raw_value = [&](const char* key, bool& found) -> std::string { + if (config.has_printer_setting(printer_name, key)) { + found = true; + return config.get_printer_setting(printer_name, key); + } + if (fallback_to_global && config.has("presets", key)) { + found = true; + return config.get("presets", key); + } + found = false; + return std::string{}; + }; + std::vector parts; + auto load_bools = [&](const char* key) { + auto& vals = project_config.option(key)->values; + vals.clear(); + bool found = false; + const std::string s = raw_value(key, found); + if (found && !s.empty()) { + boost::algorithm::split(parts, s, boost::algorithm::is_any_of(",")); + for (const auto& p : parts) + vals.push_back(p == "1"); + } + vals.resize(n_filaments, false); + }; + auto load_strings = [&](const char* key) { + auto& vals = project_config.option(key)->values; + vals.clear(); + bool found = false; + const std::string s = raw_value(key, found); + if (found && !s.empty()) { + boost::algorithm::split(parts, s, boost::algorithm::is_any_of("|")); + vals = parts; + } + vals.resize(n_filaments, std::string{}); + }; + + load_bools("filament_is_mixed"); + load_strings("filament_mixed_components"); + load_strings("filament_mixed_sublayer_ratios"); + load_bools("filament_mixed_gradient"); + load_strings("filament_mixed_gradient_range"); + load_bools("filament_mixed_gradient_per_part"); + + // The gradient curve is the one array whose values contain '|' themselves (it separates the + // control points), so it is stored C-style escaped rather than '|'-joined. + { + auto& vals = project_config.option("filament_mixed_gradient_curve")->values; + vals.clear(); + bool found = false; + const std::string s = raw_value("filament_mixed_gradient_curve", found); + if (found && !s.empty()) { + std::vector curves; + if (unescape_strings_cstyle(s, curves)) + vals = std::move(curves); + } + vals.resize(n_filaments, std::string{}); + // Heal legacy corruption: clear any non-empty slot that ended up with < 2 points + // (e.g. a curve split across slots by the old "|" delimiter). Falls back to linear. + Slic3r::sanitize_mixed_gradient_curve_array(vals); + } +} + +void PresetBundle::update_selections(AppConfig& config) +{ + std::string initial_printer_profile_name = printers.get_selected_preset_name(); // Orca: load from orca_presets - std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); - std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); + std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); + std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); // Selects the profiles, which were selected at the last application close. prints.select_preset_by_name_strict(initial_print_profile_name); @@ -2743,8 +2889,8 @@ void PresetBundle::update_selections(AppConfig &config) // Load it even if the current printer technology is SLA. // The possibly excessive filament names will be later removed with this->update_multi_material_filament_presets() // once the FFF technology gets selected. - this->filament_presets = { filaments.get_selected_preset_name() }; - for (unsigned int i = 1; i < 1000; ++ i) { + this->filament_presets = {filaments.get_selected_preset_name()}; + for (unsigned int i = 1; i < 1000; ++i) { char name[64]; sprintf(name, "filament_%02u", i); auto f_name = config.get_printer_setting(initial_printer_profile_name, name); @@ -2765,14 +2911,18 @@ void PresetBundle::update_selections(AppConfig &config) std::vector multi_filament_colors; if (config.has_printer_setting(initial_printer_profile_name, "filament_multi_colors")) { - boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), boost::algorithm::is_any_of(",")); + boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), + boost::algorithm::is_any_of(",")); } - if (multi_filament_colors.size() == 0) project_config.option("filament_multi_colour")->values = filament_colors; - else project_config.option("filament_multi_colour")->values = multi_filament_colors; + if (multi_filament_colors.size() == 0) + project_config.option("filament_multi_colour")->values = filament_colors; + else + project_config.option("filament_multi_colour")->values = multi_filament_colors; std::vector filament_color_types; if (config.has_printer_setting(initial_printer_profile_name, "filament_color_types")) { - boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), boost::algorithm::is_any_of(",")); + boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), + boost::algorithm::is_any_of(",")); } filament_color_types.resize(filament_presets.size(), "1"); project_config.option("filament_colour_type")->values = filament_color_types; @@ -2788,26 +2938,36 @@ void PresetBundle::update_selections(AppConfig &config) std::vector extruder_ams_count_str; if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { - boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); + boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), + boost::algorithm::is_any_of(",")); } this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); std::vector matrix; if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), + boost::algorithm::is_any_of("|")); auto flush_volumes_matrix = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), flush_volumes_matrix.end()); + project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), + flush_volumes_matrix.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_vector")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), + boost::algorithm::is_any_of("|")); auto flush_volumes_vector = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), flush_volumes_vector.end()); + project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), + flush_volumes_vector.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_multiplier")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), + boost::algorithm::is_any_of("|")); auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), flush_multipliers.end()); + project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), + flush_multipliers.end()); } + // No global fallback here: on a printer change the legacy shared keys describe another + // printer's filament list, so absent per-printer keys must clear the mixes, not revive them. + load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), false); // Update visibility of presets based on their compatibility with the active printer. // Always try to select a compatible print and filament preset to the current printer preset, @@ -2817,26 +2977,27 @@ void PresetBundle::update_selections(AppConfig &config) this->update_multi_material_filament_presets(); std::string first_visible_filament_name; - for (auto & fp : filament_presets) { + for (auto& fp : filament_presets) { // Orca: also match the ORCA_DEFAULT_FILAMENT_PLACEHOLDER placeholder. update_compatible_internal // iterates from m_num_default_presets, so the placeholder's is_compatible flag // stays true and the not-found/visible/compatible predicate alone would miss it. - if (auto it = filaments.find_preset_internal(fp); fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { + if (auto it = filaments.find_preset_internal(fp); + fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { if (first_visible_filament_name.empty()) first_visible_filament_name = filaments.first_compatible().name; fp = first_visible_filament_name; } } - } // Load selections (current print, current filaments, current printer) from config.ini // This is done on application start up or after updates are applied. -void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& preferred_selection/* = PresetPreferences()*/) +void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& preferred_selection /* = PresetPreferences()*/) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, preferred printer_model_id %1%")%preferred_selection.printer_model_id; - // Update visibility of presets based on application vendor / model / variant configuration. - this->load_installed_printers(config); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": enter, preferred printer_model_id %1%") % preferred_selection.printer_model_id; + // Update visibility of presets based on application vendor / model / variant configuration. + this->load_installed_printers(config); // Update visibility of filament and sla material presets this->load_installed_filaments(config); @@ -2845,7 +3006,7 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // Parse the initial print / filament / printer profile names. // std::string initial_sla_print_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_PRINT_NAME)); // std::string initial_sla_material_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_MATERIALS_NAME)); - std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", PRESET_PRINTER_NAME)); + std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", PRESET_PRINTER_NAME)); // Activate print / filament / printer profiles from either the config, // or from the preferred_model_id suggestion passed in by ConfigWizard. @@ -2853,25 +3014,26 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // Do not select alternate profiles for the print / filament profiles as those presets // will be selected by the following call of this->update_compatible(PresetSelectCompatibleType::Always). - const Preset *initial_printer = printers.find_preset(initial_printer_profile_name); + const Preset* initial_printer = printers.find_preset(initial_printer_profile_name); // If executed due to a Config Wizard update, preferred_printer contains the first newly installed printer, otherwise nullptr. - const Preset *preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, preferred_selection.printer_variant); + const Preset* preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, + preferred_selection.printer_variant); printers.select_preset_by_name(preferred_printer ? preferred_printer->name : initial_printer_profile_name, true); CNumericLocalesSetter locales_setter; // Orca: load from orca_presets // const auto os_presets = config.get_machine_settings(initial_printer_profile_name); - std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); - std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); + std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); + std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); - //BBS: set default print/filament profiles to BBL's default setting - if (preferred_printer) - { + // BBS: set default print/filament profiles to BBL's default setting + if (preferred_printer) { const std::string& prefered_print_profile = preferred_printer->config.opt_string("default_print_profile"); if ((!initial_print_profile_name.compare("Default Setting")) && (prefered_print_profile.size() > 0)) initial_print_profile_name = prefered_print_profile; - const std::vector& prefered_filament_profiles = preferred_printer->config.option("default_filament_profile")->values; + const std::vector& prefered_filament_profiles = + preferred_printer->config.option("default_filament_profile")->values; if ((!initial_filament_profile_name.compare(ORCA_DEFAULT_FILAMENT_PLACEHOLDER)) && (prefered_filament_profiles.size() > 0)) { // Check if preferred filament is visible const Preset* preferred_preset = this->filaments.find_preset(prefered_filament_profiles[0], false); @@ -2885,15 +3047,15 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // Selects the profile, leaves it to -1 if the initial profile name is empty or if it was not found. prints.select_preset_by_name_strict(initial_print_profile_name); filaments.select_preset_by_name_strict(initial_filament_profile_name); - // sla_prints.select_preset_by_name_strict(initial_sla_print_profile_name); + // sla_prints.select_preset_by_name_strict(initial_sla_print_profile_name); // sla_materials.select_preset_by_name_strict(initial_sla_material_profile_name); // Load the names of the other filament profiles selected for a multi-material printer. // Load it even if the current printer technology is SLA. // The possibly excessive filament names will be later removed with this->update_multi_material_filament_presets() // once the FFF technology gets selected. - this->filament_presets = { filaments.get_selected_preset_name() }; - for (unsigned int i = 1; i < 1000; ++ i) { + this->filament_presets = {filaments.get_selected_preset_name()}; + for (unsigned int i = 1; i < 1000; ++i) { char name[64]; sprintf(name, "filament_%02u", i); auto f_name = config.get_printer_setting(initial_printer_profile_name, name); @@ -2915,14 +3077,18 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p std::vector multi_filament_colors; if (config.has_printer_setting(initial_printer_profile_name, "filament_multi_colors")) { - boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), boost::algorithm::is_any_of(",")); + boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), + boost::algorithm::is_any_of(",")); } - if (multi_filament_colors.size() == 0) project_config.option("filament_multi_colour")->values = filament_colors; - else project_config.option("filament_multi_colour")->values = multi_filament_colors; + if (multi_filament_colors.size() == 0) + project_config.option("filament_multi_colour")->values = filament_colors; + else + project_config.option("filament_multi_colour")->values = multi_filament_colors; std::vector filament_color_types; if (config.has_printer_setting(initial_printer_profile_name, "filament_color_types")) { - boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), boost::algorithm::is_any_of(",")); + boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), + boost::algorithm::is_any_of(",")); } filament_color_types.resize(filament_presets.size(), "1"); project_config.option("filament_colour_type")->values = filament_color_types; @@ -2938,26 +3104,34 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p std::vector extruder_ams_count_str; if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { - boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); + boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), + boost::algorithm::is_any_of(",")); } this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); std::vector matrix; if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), + boost::algorithm::is_any_of("|")); auto flush_volumes_matrix = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), flush_volumes_matrix.end()); + project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), + flush_volumes_matrix.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_vector")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), + boost::algorithm::is_any_of("|")); auto flush_volumes_vector = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), flush_volumes_vector.end()); + project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), + flush_volumes_vector.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_multiplier")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), + boost::algorithm::is_any_of("|")); auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), flush_multipliers.end()); + project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), + flush_multipliers.end()); } + load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), true); // Update visibility of presets based on their compatibility with the active printer. // Always try to select a compatible print and filament preset to the current printer preset, @@ -2967,17 +3141,17 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p this->update_multi_material_filament_presets(); if (initial_printer != nullptr && (preferred_printer == nullptr || initial_printer == preferred_printer)) { - // Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new printer. - // Only run this code if just a filament / SLA material was installed by Config Wizard for an active Printer. + // Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new + // printer. Only run this code if just a filament / SLA material was installed by Config Wizard for an active Printer. auto printer_technology = printers.get_selected_preset().printer_technology(); - if (printer_technology == ptFFF && ! preferred_selection.filament.empty()) { + if (printer_technology == ptFFF && !preferred_selection.filament.empty()) { std::string preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_FILAMENT, preferred_selection.filament); if (auto it = filaments.find_preset_internal(preferred_preset_name); - it != filaments.end() && (it->name == preferred_preset_name ) && it->is_visible && it->is_compatible) { + it != filaments.end() && (it->name == preferred_preset_name) && it->is_visible && it->is_compatible) { filaments.select_preset_by_name_strict(preferred_preset_name); this->filament_presets.front() = filaments.get_selected_preset_name(); } - } else if (printer_technology == ptSLA && ! preferred_selection.sla_material.empty()) { + } else if (printer_technology == ptSLA && !preferred_selection.sla_material.empty()) { std::string preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_SLA_MATERIAL, preferred_selection.sla_material); if (auto it = sla_materials.find_preset_internal(preferred_preset_name); it != sla_materials.end() && it->is_visible && it->is_compatible) @@ -2986,17 +3160,18 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p } std::string first_visible_filament_name; - for (auto & fp : filament_presets) { + for (auto& fp : filament_presets) { // Orca: also match the ORCA_DEFAULT_FILAMENT_PLACEHOLDER placeholder — see update_selections. - if (auto it = filaments.find_preset_internal(fp); fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { + if (auto it = filaments.find_preset_internal(fp); + fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { if (first_visible_filament_name.empty()) first_visible_filament_name = filaments.first_compatible().name; fp = first_visible_filament_name; } } - const Preset& current_printer = printers.get_selected_preset(); - const Preset* base_printer = printers.get_preset_base(current_printer); + const Preset& current_printer = printers.get_selected_preset(); + const Preset* base_printer = printers.get_preset_base(current_printer); bool use_default_nozzle_volume_type = true; if (base_printer) { std::string prev_nozzle_volume_type = config.get_nozzle_volume_types_from_config(base_printer->name); @@ -3009,11 +3184,12 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p } if (use_default_nozzle_volume_type) { - project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; + project_config.option("nozzle_volume_type")->values = + current_printer.config.option("default_nozzle_volume_type")->values; } else { // Orca: make sure `nozzle_volume_type` not shorter than `default_nozzle_volume_type`, otherwise we got array out of bound access // later in `Tab::switch_excluder` - auto& opt = project_config.option("nozzle_volume_type")->values; + auto& opt = project_config.option("nozzle_volume_type")->values; const auto& opt_default = current_printer.config.option("default_nozzle_volume_type")->values; while (opt.size() < opt_default.size()) { opt.emplace_back(opt_default[opt.size()]); @@ -3027,15 +3203,17 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p if (!initial_physical_printer_name.empty()) physical_printers.select_printer(initial_physical_printer_name); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, preferred printer_model_id %1%")%preferred_selection.printer_model_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": finished, preferred printer_model_id %1%") % preferred_selection.printer_model_id; } // Export selections (current print, current filaments, current printer) into config.ini // BBS: change directories by design -void PresetBundle::export_selections(AppConfig &config) +void PresetBundle::export_selections(AppConfig& config) { - assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1); - //assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front()); + assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1); + // assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || + // filaments.get_selected_preset_name() == filament_presets.front()); config.clear_section("presets"); auto printer_name = printers.get_selected_preset_name(); config.set("presets", PRESET_PRINTER_NAME, printer_name); @@ -3052,7 +3230,7 @@ void PresetBundle::export_selections(AppConfig &config) config.clear_printer_settings(printer_name); config.set_printer_setting(printer_name, PRESET_PRINTER_NAME, printer_name); config.set_printer_setting(printer_name, PRESET_PRINT_NAME, prints.get_selected_preset_name()); - config.set_printer_setting(printer_name, PRESET_FILAMENT_NAME, filament_presets.front()); + config.set_printer_setting(printer_name, PRESET_FILAMENT_NAME, filament_presets.front()); config.set_printer_setting(printer_name, "curr_bed_type", config.get("curr_bed_type")); for (unsigned i = 1; i < filament_presets.size(); ++i) { char name[64]; @@ -3062,58 +3240,92 @@ void PresetBundle::export_selections(AppConfig &config) } // Load project config data into app config CNumericLocalesSetter locales_setter; - std::string filament_colors = boost::algorithm::join(project_config.option("filament_colour")->values, ","); + std::string filament_colors = boost::algorithm::join(project_config.option("filament_colour")->values, ","); config.set_printer_setting(printer_name, "filament_colors", filament_colors); // Load filament multi color data into app config - std::string filament_multi_colors = boost::algorithm::join(project_config.option("filament_multi_colour")->values, ","); + std::string filament_multi_colors = boost::algorithm::join(project_config.option("filament_multi_colour")->values, + ","); config.set_printer_setting(printer_name, "filament_multi_colors", filament_multi_colors); // Load filament color type data into app config - std::string filament_color_types = boost::algorithm::join(project_config.option("filament_colour_type")->values, ","); + std::string filament_color_types = boost::algorithm::join(project_config.option("filament_colour_type")->values, + ","); config.set_printer_setting(printer_name, "filament_color_types", filament_color_types); // Load ams counts data into app config - std::string extruder_ams_count_str = boost::algorithm::join(save_extruder_ams_count_to_string(this->extruder_ams_counts), ","); + std::string extruder_ams_count_str = boost::algorithm::join(save_extruder_ams_count_to_string(this->extruder_ams_counts), ","); config.set_printer_setting(printer_name, "extruder_ams_count", extruder_ams_count_str); std::string flush_volumes_matrix = boost::algorithm::join(project_config.option("flush_volumes_matrix")->values | - boost::adaptors::transformed(static_cast(std::to_string)), - "|"); + boost::adaptors::transformed( + static_cast(std::to_string)), + "|"); config.set_printer_setting(printer_name, "flush_volumes_matrix", flush_volumes_matrix); std::string flush_volumes_vector = boost::algorithm::join(project_config.option("flush_volumes_vector")->values | - boost::adaptors::transformed(static_cast(std::to_string)), - "|"); + boost::adaptors::transformed( + static_cast(std::to_string)), + "|"); config.set_printer_setting(printer_name, "flush_volumes_vector", flush_volumes_vector); - std::string flush_multiplier_str = boost::algorithm::join(project_config.option("flush_multiplier")->values | - boost::adaptors::transformed(static_cast(std::to_string)), + boost::adaptors::transformed( + static_cast(std::to_string)), "|"); config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str); + // Mixed-color filament metadata goes into the per-printer snapshot next to the filament list + // it indexes (see load_mixed_filament_settings). Bools are ','-joined and the component, ratio + // and range strings '|'-joined; the gradient curve is escaped instead, as it contains '|'. + auto join_bools = [](const std::vector& vals) { + std::string s; + for (size_t i = 0; i < vals.size(); ++i) { + if (i > 0) + s += ","; + s += (vals[i] ? "1" : "0"); + } + return s; + }; + if (auto* opt = project_config.option("filament_is_mixed")) + config.set_printer_setting(printer_name, "filament_is_mixed", join_bools(opt->values)); + if (auto* opt = project_config.option("filament_mixed_components")) + config.set_printer_setting(printer_name, "filament_mixed_components", boost::algorithm::join(opt->values, "|")); + if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) + config.set_printer_setting(printer_name, "filament_mixed_sublayer_ratios", boost::algorithm::join(opt->values, "|")); + if (auto* opt = project_config.option("filament_mixed_gradient")) + config.set_printer_setting(printer_name, "filament_mixed_gradient", join_bools(opt->values)); + if (auto* opt = project_config.option("filament_mixed_gradient_range")) + config.set_printer_setting(printer_name, "filament_mixed_gradient_range", boost::algorithm::join(opt->values, "|")); + if (auto* opt = project_config.option("filament_mixed_gradient_curve")) + config.set_printer_setting(printer_name, "filament_mixed_gradient_curve", escape_strings_cstyle(opt->values)); + if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) + config.set_printer_setting(printer_name, "filament_mixed_gradient_per_part", join_bools(opt->values)); + // BBS - //config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); - //config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); - //config.set("presets", "physical_printer", physical_printers.get_selected_full_printer_name()); - //BBS: add config related log - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1%, print %2%, filaments[0] %3% ")%printers.get_selected_preset_name() % prints.get_selected_preset_name() %filament_presets[0]; + // config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); + // config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); + // config.set("presets", "physical_printer", physical_printers.get_selected_full_printer_name()); + // BBS: add config related log + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": printer %1%, print %2%, filaments[0] %3% ") % printers.get_selected_preset_name() % + prints.get_selected_preset_name() % filament_presets[0]; } // BBS -void PresetBundle::set_num_filaments(unsigned int n, std::vector new_colors) { +void PresetBundle::set_num_filaments(unsigned int n, std::vector new_colors) +{ int old_filament_count = this->filament_presets.size(); if (n > old_filament_count && old_filament_count != 0) filament_presets.resize(n, filament_presets.back()); else { filament_presets.resize(n); } - ConfigOptionStrings* filament_color = project_config.option("filament_colour"); - ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); - ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option("filament_map"); - ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); - ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings* filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); filament_color->resize(n); // Sync filament multi colour @@ -3127,13 +3339,30 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); + // Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink + // with the filament count exactly like filament_colour above. + if (auto* opt = project_config.option("filament_is_mixed")) + opt->values.resize(n, false); + if (auto* opt = project_config.option("filament_mixed_components")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_gradient")) + opt->values.resize(n, false); + if (auto* opt = project_config.option("filament_mixed_gradient_range")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_gradient_curve")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) + opt->values.resize(n, false); + // BBS set new filament color to new_color if (old_filament_count < n) { if (!new_colors.empty()) { for (int i = old_filament_count; i < n; i++) { - filament_color->values[i] = new_colors[i - old_filament_count]; + filament_color->values[i] = new_colors[i - old_filament_count]; filament_multi_color->values[i] = new_colors[i - old_filament_count]; - filament_color_type->values[i] = "1"; // default color type + filament_color_type->values[i] = "1"; // default color type } } } @@ -3148,12 +3377,12 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) else { filament_presets.resize(n); } - ConfigOptionStrings* filament_color = project_config.option("filament_colour"); - ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); - ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option("filament_map"); - ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); - ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings* filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); filament_color->resize(n); // Sync filament multi colour @@ -3167,13 +3396,30 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); - //BBS set new filament color to new_color + // Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink + // with the filament count exactly like filament_colour above. + if (auto* opt = project_config.option("filament_is_mixed")) + opt->values.resize(n, false); + if (auto* opt = project_config.option("filament_mixed_components")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_gradient")) + opt->values.resize(n, false); + if (auto* opt = project_config.option("filament_mixed_gradient_range")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_gradient_curve")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) + opt->values.resize(n, false); + + // BBS set new filament color to new_color if (old_filament_count < n) { if (!new_color.empty()) { for (unsigned i = old_filament_count; i < n; i++) { - filament_color->values[i] = new_color; + filament_color->values[i] = new_color; filament_multi_color->values[i] = new_color; - filament_color_type->values[i] = "1"; // default color type + filament_color_type->values[i] = "1"; // default color type } } } @@ -3189,8 +3435,8 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) // update edited_preset { - Preset& edited_preset = filaments.get_edited_preset(); - bool edited_preset_deleted = true; + Preset& edited_preset = filaments.get_edited_preset(); + bool edited_preset_deleted = true; for (std::string filament_preset_name : filament_presets) { if (filament_preset_name == edited_preset.name) { edited_preset_deleted = false; @@ -3201,12 +3447,12 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) } } - ConfigOptionStrings *filament_color = project_config.option("filament_colour"); - ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); - ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option("filament_map"); - ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); - ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings* filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); if (filament_color->values.size() > to_del_flament_id) { filament_color->values.erase(filament_color->values.begin() + to_del_flament_id); if (filament_map->values.size() > to_del_flament_id) { @@ -3218,8 +3464,7 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) if (filament_volume_map->values.size() > to_del_flament_id) { filament_volume_map->values.erase(filament_volume_map->values.begin() + to_del_flament_id); } - } - else { + } else { filament_color->values.resize(to_del_flament_id); filament_map->values.resize(to_del_flament_id, 1); filament_nozzle_map->values.resize(to_del_flament_id, 0); @@ -3239,20 +3484,67 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) erase_or_resize(filament_color_type->values); erase_or_resize(ams_multi_color_filment); + // Mixed-color metadata. Component IDs reference other slots by 1-based index, so a deleted + // *physical* filament must be remapped out of every mix before the arrays themselves shrink. + // Deleting a mixed slot needs no remap (nothing references a mixed slot as a component). + { + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* comp_opt = project_config.option("filament_mixed_components"); + if (is_mixed_opt && comp_opt) { + bool del_is_physical = (to_del_flament_id >= is_mixed_opt->values.size() || !is_mixed_opt->values[to_del_flament_id]); + if (del_is_physical) + remap_mixed_components_on_delete(is_mixed_opt->values, comp_opt->values, to_del_flament_id + 1); + } + if (is_mixed_opt) + erase_or_resize(is_mixed_opt->values); + if (comp_opt) + erase_or_resize(comp_opt->values); + } + if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) + erase_or_resize(opt->values); + if (auto* opt = project_config.option("filament_mixed_gradient")) + erase_or_resize(opt->values); + if (auto* opt = project_config.option("filament_mixed_gradient_range")) + erase_or_resize(opt->values); + if (auto* opt = project_config.option("filament_mixed_gradient_curve")) + erase_or_resize(opt->values); + if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) + erase_or_resize(opt->values); + update_multi_material_filament_presets(to_del_flament_id); } +bool PresetBundle::is_mixed_filament(size_t idx) const +{ + auto* opt = project_config.option("filament_is_mixed"); + return opt && idx < opt->values.size() && opt->values[idx]; +} + +size_t PresetBundle::num_mixed_filaments() const +{ + auto* opt = project_config.option("filament_is_mixed"); + return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true)); +} + +std::vector PresetBundle::physical_filament_config_indices() const +{ + std::vector indices; + for (size_t i = 0; i < filament_presets.size(); ++i) + if (!is_mixed_filament(i)) + indices.push_back(i); + return indices; +} void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) { combox_info.clear(); - for (auto &entry : filament_ams_list) { - auto &ams = entry.second; - auto filament_id = ams.opt_string("filament_id", 0u); - auto filament_color = ams.opt_string("filament_colour", 0u); - auto ams_name = ams.opt_string("tray_name", 0u); - auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); - auto filament_multi_color = ams.opt("filament_multi_colour")->values; + for (auto& entry : filament_ams_list) { + auto& ams = entry.second; + auto filament_id = ams.opt_string("filament_id", 0u); + auto filament_color = ams.opt_string("filament_colour", 0u); + auto ams_name = ams.opt_string("tray_name", 0u); + auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + auto filament_multi_color = ams.opt("filament_multi_colour")->values; if (filament_id.empty()) { continue; } @@ -3263,15 +3555,18 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) combox_info.ams_names.push_back(ams_name); continue; } - auto iter = std::find_if(filaments.begin(), filaments.end(), - [this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); + auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id](auto& f) { + return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; + }); if (iter == filaments.end()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(": filament_id %1% not found or system or compatible") % filament_id; auto filament_type = ams.opt_string("filament_type", 0u); if (!filament_type.empty()) { filament_type = "Generic " + filament_type; - iter = std::find_if(filaments.begin(), filaments.end(), - [&filament_type](auto &f) { return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); }); + iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto& f) { + return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); + }); } if (iter == filaments.end()) { // Prefer old selection @@ -3282,7 +3577,8 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) combox_info.ams_names.push_back(ams_name); continue; } - iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) { return f.is_compatible && f.is_system; }); + iter = std::find_if(filaments.begin(), filaments.end(), + [&filament_type](auto& f) { return f.is_compatible && f.is_system; }); if (iter == filaments.end()) continue; } @@ -3295,13 +3591,18 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) } } -unsigned int PresetBundle::sync_ams_list(std::vector> &unknowns, bool use_map, std::map &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only) +unsigned int PresetBundle::sync_ams_list(std::vector>& unknowns, + bool use_map, + std::map& maps, + bool enable_append, + MergeFilamentInfo& merge_info, + bool color_only) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append; std::vector ams_filament_presets; std::vector ams_filament_colors; std::vector ams_filament_color_types; - std::vector ams_array_maps; + std::vector ams_array_maps; ams_multi_color_filment.clear(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_ams_list size: %1%") % filament_ams_list.size(); struct AmsInfo @@ -3309,24 +3610,24 @@ unsigned int PresetBundle::sync_ams_list(std::vector mutli_filament_color; }; auto is_double_extruder = get_printer_extruder_count() == 2; std::vector ams_infos; - int index = 0; - for (auto &entry : filament_ams_list) { - auto & ams = entry.second; - auto filament_id = ams.opt_string("filament_id", 0u); - auto filament_color = ams.opt_string("filament_colour", 0u); - auto filament_color_type = ams.opt_string("filament_colour_type", 0u); - auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + int index = 0; + for (auto& entry : filament_ams_list) { + auto& ams = entry.second; + auto filament_id = ams.opt_string("filament_id", 0u); + auto filament_color = ams.opt_string("filament_colour", 0u); + auto filament_color_type = ams.opt_string("filament_colour_type", 0u); + auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); auto filament_multi_color = ams.opt("filament_multi_colour")->values; - auto ams_id = ams.opt_string("ams_id", 0u); - auto slot_id = ams.opt_string("slot_id", 0u); - auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); + auto ams_id = ams.opt_string("ams_id", 0u); + auto slot_id = ams.opt_string("slot_id", 0u); + auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color}); AMSMapInfo temp = {ams_id, slot_id}; ams_array_maps.push_back(temp); @@ -3338,7 +3639,7 @@ unsigned int PresetBundle::sync_ams_list(std::vectorconfig.opt_string("filament_type", 0u)); if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) { - iter = it; - best_len = preset_type.size(); + iter = it; + best_len = preset_type.size(); filament_type = "Generic " + it->config.opt_string("filament_type", 0u); } } @@ -3413,26 +3714,28 @@ unsigned int PresetBundle::sync_ams_list(std::vectorname, filament_type) ? - (has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : - L("The filament model is unknown. Generic filament presets will be used.")) : - (has_type ? L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") : - L("The filament model is unknown. A random filament preset will be used."))); + unknowns.emplace_back( + &ams, + boost::algorithm::starts_with(iter->name, filament_type) ? + (has_type ? + L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : + L("The filament model is unknown. Generic filament presets will be used.")) : + (has_type ? + L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") : + L("The filament model is unknown. A random filament preset will be used."))); filament_id = iter->filament_id; } ams_filament_presets.push_back(iter->name); @@ -3443,12 +3746,83 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_colour"); - ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts * filament_map = project_config.option("filament_map"); - ConfigOptionInts * filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + + // Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical + // filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in + // would let AMS mapping overwrite it and would break the physical-first slot ordering the + // rest of the feature relies on. The slots are re-appended verbatim after the sync. + struct MixedSlotSnapshot + { + std::string preset; + std::string color; + std::string color_type; + std::string mixed_components; + std::string mixed_sublayer_ratios; + bool mixed_gradient = false; + std::string mixed_gradient_range; + std::string mixed_gradient_curve; + bool mixed_gradient_per_part = false; + }; + std::vector mixed_snapshots; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* mixed_comp_opt = project_config.option("filament_mixed_components"); + auto* mixed_ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + auto* mixed_gradient_opt = project_config.option("filament_mixed_gradient"); + auto* mixed_grad_range_opt = project_config.option("filament_mixed_gradient_range"); + auto* mixed_grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); + auto* mixed_per_part_opt = project_config.option("filament_mixed_gradient_per_part"); + if (is_mixed_opt) { + for (size_t i = 0; i < is_mixed_opt->values.size() && i < this->filament_presets.size(); ++i) { + if (!is_mixed_opt->values[i]) + continue; + MixedSlotSnapshot snap; + snap.preset = this->filament_presets[i]; + snap.color = (i < filament_color->values.size()) ? filament_color->values[i] : ""; + snap.color_type = (i < filament_color_type->values.size()) ? filament_color_type->values[i] : ""; + if (mixed_comp_opt && i < mixed_comp_opt->values.size()) + snap.mixed_components = mixed_comp_opt->values[i]; + if (mixed_ratios_opt && i < mixed_ratios_opt->values.size()) + snap.mixed_sublayer_ratios = mixed_ratios_opt->values[i]; + if (mixed_gradient_opt && i < mixed_gradient_opt->values.size()) + snap.mixed_gradient = mixed_gradient_opt->values[i]; + if (mixed_grad_range_opt && i < mixed_grad_range_opt->values.size()) + snap.mixed_gradient_range = mixed_grad_range_opt->values[i]; + if (mixed_grad_curve_opt && i < mixed_grad_curve_opt->values.size()) + snap.mixed_gradient_curve = mixed_grad_curve_opt->values[i]; + if (mixed_per_part_opt && i < mixed_per_part_opt->values.size()) + snap.mixed_gradient_per_part = mixed_per_part_opt->values[i]; + mixed_snapshots.push_back(snap); + } + if (!mixed_snapshots.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": stripping " << mixed_snapshots.size() + << " mixed filament slot(s) before AMS sync"; + size_t phys_count = this->filament_presets.size() - mixed_snapshots.size(); + this->filament_presets.resize(phys_count); + filament_color->values.resize(phys_count); + filament_color_type->values.resize(phys_count); + filament_map->values.resize(phys_count, 1); + is_mixed_opt->values.resize(phys_count); + if (mixed_comp_opt) + mixed_comp_opt->values.resize(phys_count); + if (mixed_ratios_opt) + mixed_ratios_opt->values.resize(phys_count); + if (mixed_gradient_opt) + mixed_gradient_opt->values.resize(phys_count); + if (mixed_grad_range_opt) + mixed_grad_range_opt->values.resize(phys_count); + if (mixed_grad_curve_opt) + mixed_grad_curve_opt->values.resize(phys_count); + if (mixed_per_part_opt) + mixed_per_part_opt->values.resize(phys_count); + } + } + if (color_only) { - auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { + auto get_map_index = [&ams_infos](const std::vector& infos, const AMSMapInfo& temp) { for (int i = 0; i < infos.size(); i++) { if (infos[i].slot_id == temp.slot_id && infos[i].ams_id == temp.ams_id) { ams_infos[i].is_map = true; @@ -3464,7 +3838,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_multi_colour"); + ConfigOptionStrings* project_multi_color = project_config.option("filament_multi_colour"); if (project_multi_color) { for (size_t i = 0; i < std::min(exist_multi_color_filment.size(), project_multi_color->values.size()); i++) { std::vector colors = split_string(project_multi_color->values[i], ' '); @@ -3483,7 +3857,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector= 0 && valid_index < int(ams_filament_colors.size()) && !ams_filament_colors[valid_index].empty()) { exist_colors[i] = ams_filament_colors[valid_index]; - mapped_any = true; + mapped_any = true; if (valid_index < int(ams_multi_color_filment.size()) && !ams_multi_color_filment[valid_index].empty()) { exist_multi_color_filment[i] = ams_multi_color_filment[valid_index]; } else { @@ -3508,11 +3882,11 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues = exist_colors; + filament_color->values = exist_colors; ams_multi_color_filment = exist_multi_color_filment; merge_info.merges.clear(); } else if (use_map) { - auto check_has_merge_info = [](std::map &maps, MergeFilamentInfo &merge_info, int exist_colors_size) { + auto check_has_merge_info = [](std::map& maps, MergeFilamentInfo& merge_info, int exist_colors_size) { std::set done; for (auto it_i = maps.begin(); it_i != maps.end(); ++it_i) { std::vector same_ams; @@ -3521,7 +3895,7 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfirst) != done.end()) { continue; } - if (it_i->second.slot_id == "" || it_i->second.ams_id == ""){ + if (it_i->second.slot_id == "" || it_i->second.ams_id == "") { continue; } if (it_i->second.slot_id == it_j->second.slot_id && it_i->second.ams_id == it_j->second.ams_id) { @@ -3534,8 +3908,8 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.size()); - auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { + check_has_merge_info(maps, merge_info, filament_color->values.size()); + auto get_map_index = [&ams_infos](const std::vector& infos, const AMSMapInfo& temp) { for (int i = 0; i < infos.size(); i++) { if (infos[i].slot_id == temp.slot_id && infos[i].ams_id == temp.ams_id) { ams_infos[i].is_map = true; @@ -3545,8 +3919,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector need_append_colors; - auto exist_colors = filament_color->values; - auto exist_color_types = filament_color_type->values; + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; auto exist_filament_presets = this->filament_presets; std::vector> exist_multi_color_filment; exist_multi_color_filment.resize(exist_colors.size()); @@ -3554,54 +3928,57 @@ unsigned int PresetBundle::sync_ams_list(std::vector= 0 && valid_index < ams_filament_presets.size()) { - exist_colors[i] = ams_filament_colors[valid_index]; - exist_color_types[i] = ams_filament_color_types[valid_index]; - exist_filament_presets[i] = ams_filament_presets[valid_index]; + exist_colors[i] = ams_filament_colors[valid_index]; + exist_color_types[i] = ams_filament_color_types[valid_index]; + exist_filament_presets[i] = ams_filament_presets[valid_index]; exist_multi_color_filment[i] = ams_multi_color_filment[valid_index]; } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)"; } } } - for (size_t i = 0; i < ams_infos.size(); i++) {// check append + for (size_t i = 0; i < ams_infos.size(); i++) { // check append if (ams_infos[i].valid) { if (i >= ams_filament_presets.size()) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)"; continue; } - ams_infos[i].filament_preset = ams_filament_presets[i]; + ams_infos[i].filament_preset = ams_filament_presets[i]; ams_infos[i].mutli_filament_color = ams_multi_color_filment[i]; if (!ams_infos[i].is_map) { need_append_colors.emplace_back(ams_infos[i]); - ams_filament_colors[i] = ""; + ams_filament_colors[i] = ""; ams_filament_color_types[i] = ""; - ams_filament_presets[i] = ""; - ams_multi_color_filment[i] = std::vector(); + ams_filament_presets[i] = ""; + ams_multi_color_filment[i] = std::vector(); } - } - else { - ams_filament_colors[i] = ""; + } else { + ams_filament_colors[i] = ""; ams_filament_color_types[i] = ""; - ams_filament_presets[i] = ""; - ams_multi_color_filment[i] = std::vector(); + ams_filament_presets[i] = ""; + ams_multi_color_filment[i] = std::vector(); } } - //delete redundant color - ams_filament_colors.erase(std::remove_if(ams_filament_colors.begin(), ams_filament_colors.end(), [](std::string &value) { return value.empty(); }), + // delete redundant color + ams_filament_colors.erase(std::remove_if(ams_filament_colors.begin(), ams_filament_colors.end(), + [](std::string& value) { return value.empty(); }), ams_filament_colors.end()); - ams_filament_color_types.erase(std::remove_if(ams_filament_color_types.begin(), ams_filament_color_types.end(), [](std::string &value) { return value.empty(); }), + ams_filament_color_types.erase(std::remove_if(ams_filament_color_types.begin(), ams_filament_color_types.end(), + [](std::string& value) { return value.empty(); }), ams_filament_color_types.end()); - ams_filament_presets.erase(std::remove_if(ams_filament_presets.begin(), ams_filament_presets.end(), [](std::string &value) { return value.empty(); }), + ams_filament_presets.erase(std::remove_if(ams_filament_presets.begin(), ams_filament_presets.end(), + [](std::string& value) { return value.empty(); }), ams_filament_presets.end()); ams_multi_color_filment.erase(std::remove_if(ams_multi_color_filment.begin(), ams_multi_color_filment.end(), - [](std::vector &value) { return value.empty(); }), + [](std::vector& value) { return value.empty(); }), ams_multi_color_filment.end()); if (need_append_colors.size() > 0 && enable_append) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append"; - auto get_idx_in_array = [](std::vector &presets, std::vector &colors, const std::string &preset, const std::string &color) -> int { + auto get_idx_in_array = [](std::vector& presets, std::vector& colors, const std::string& preset, + const std::string& color) -> int { for (size_t i = 0; i < presets.size(); i++) { if (presets[i] == preset && colors[i] == color) { return i; @@ -3609,11 +3986,12 @@ unsigned int PresetBundle::sync_ams_list(std::vector= size_t(EnforcerBlockerType::ExtruderMax)){ + for (size_t i = 0; i < need_append_colors.size(); i++) { + if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER) { break; } - auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color); + auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, + need_append_colors[i].filament_color); if (idx >= 0) { continue; } @@ -3623,21 +4001,19 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues = exist_colors; + filament_color->values = exist_colors; filament_color_type->values = exist_color_types; - ams_multi_color_filment = exist_multi_color_filment; - this->filament_presets = exist_filament_presets; + ams_multi_color_filment = exist_multi_color_filment; + this->filament_presets = exist_filament_presets; filament_map->values.resize(exist_filament_presets.size(), 1); filament_volume_map->values.resize(exist_filament_presets.size(), static_cast(NozzleVolumeType::nvtStandard)); - } - else {//overwrite; - bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), - [](const AmsInfo& a) { return a.is_placeholder; }); + } else { // overwrite; + bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), [](const AmsInfo& a) { return a.is_placeholder; }); if (has_placeholders) { // Orca: merge — keep existing filaments for empty slots - auto exist_colors = filament_color->values; - auto exist_color_types = filament_color_type->values; - auto exist_presets = this->filament_presets; + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; + auto exist_presets = this->filament_presets; size_t tray_count = ams_filament_presets.size(); size_t total = std::max(tray_count, exist_presets.size()); @@ -3655,9 +4031,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector{ams_filament_colors[i]}); + result_multi_colors.push_back(i < ams_multi_color_filment.size() ? ams_multi_color_filment[i] : + std::vector{ams_filament_colors[i]}); } else if (i < exist_presets.size()) { // Empty tray or beyond tray count: keep existing filament result_colors.push_back(exist_colors[i]); @@ -3666,9 +4041,8 @@ unsigned int PresetBundle::sync_ams_list(std::vectorname : filaments.first_visible().name; result_colors.push_back("#CECECE"); @@ -3686,15 +4060,15 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.resize(total, static_cast(NozzleVolumeType::nvtStandard)); } else { // BBL: existing wholesale replace - filament_color->values = ams_filament_colors; + filament_color->values = ams_filament_colors; filament_color_type->values = ams_filament_color_types; - this->filament_presets = ams_filament_presets; + this->filament_presets = ams_filament_presets; filament_map->values.resize(ams_filament_colors.size(), 1); filament_volume_map->values.resize(ams_filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); } - auto& print_config = this->prints.get_edited_preset().config; - auto support_filament_opt = print_config.option("support_filament"); + auto& print_config = this->prints.get_edited_preset().config; + auto support_filament_opt = print_config.option("support_filament"); auto support_interface_filament_opt = print_config.option("support_interface_filament"); if (support_filament_opt->value > filament_color_type->values.size()) support_filament_opt->value = 0; @@ -3702,6 +4076,48 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalue > filament_color_type->values.size()) support_interface_filament_opt->value = 0; } + // Re-append mixed filament slots that were stripped before AMS sync + if (!mixed_snapshots.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": re-appending " << mixed_snapshots.size() << " mixed filament slot(s) after AMS sync"; + size_t new_phys_count = this->filament_presets.size(); + if (is_mixed_opt) + is_mixed_opt->values.resize(new_phys_count, (unsigned char) false); + if (mixed_comp_opt) + mixed_comp_opt->values.resize(new_phys_count); + if (mixed_ratios_opt) + mixed_ratios_opt->values.resize(new_phys_count); + if (mixed_gradient_opt) + mixed_gradient_opt->values.resize(new_phys_count, (unsigned char) false); + if (mixed_grad_range_opt) + mixed_grad_range_opt->values.resize(new_phys_count); + if (mixed_grad_curve_opt) + mixed_grad_curve_opt->values.resize(new_phys_count); + if (mixed_per_part_opt) + mixed_per_part_opt->values.resize(new_phys_count, (unsigned char) false); + + for (auto& snap : mixed_snapshots) { + this->filament_presets.push_back(snap.preset); + filament_color->values.push_back(snap.color); + filament_color_type->values.push_back(snap.color_type); + ams_multi_color_filment.push_back({snap.color}); + filament_map->values.push_back(1); + if (is_mixed_opt) + is_mixed_opt->values.push_back((unsigned char) true); + if (mixed_comp_opt) + mixed_comp_opt->values.push_back(snap.mixed_components); + if (mixed_ratios_opt) + mixed_ratios_opt->values.push_back(snap.mixed_sublayer_ratios); + if (mixed_gradient_opt) + mixed_gradient_opt->values.push_back((unsigned char) snap.mixed_gradient); + if (mixed_grad_range_opt) + mixed_grad_range_opt->values.push_back(snap.mixed_gradient_range); + if (mixed_grad_curve_opt) + mixed_grad_curve_opt->values.push_back(snap.mixed_gradient_curve); + if (mixed_per_part_opt) + mixed_per_part_opt->values.push_back((unsigned char) snap.mixed_gradient_per_part); + } + } + // Update ams_multi_color_filment update_filament_multi_color(); update_multi_material_filament_presets(); @@ -3712,36 +4128,38 @@ unsigned int PresetBundle::sync_ams_list(std::vector exsit_multi_colors; - for (auto &fil_item : ams_multi_color_filment){ - if (fil_item.empty()) break; + for (auto& fil_item : ams_multi_color_filment) { + if (fil_item.empty()) + break; if (fil_item.size() == 1) exsit_multi_colors.push_back(fil_item[0]); else { std::string colors = ""; - for (auto &color : fil_item){ - colors += color + " "; + for (auto& color : fil_item) { + colors += color + " "; } colors.erase(colors.size() - 1); // remove last space exsit_multi_colors.push_back(colors); } } - ConfigOptionStrings *filament_multi_colour = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_multi_colour = project_config.option("filament_multi_colour"); filament_multi_colour->resize(exsit_multi_colors.size()); filament_multi_colour->values = exsit_multi_colors; } -std::vector PresetBundle::get_used_tpu_filaments(const std::vector &used_filaments) +std::vector PresetBundle::get_used_tpu_filaments(const std::vector& used_filaments) { std::vector tpu_filaments; for (size_t i = 0; i < this->filament_presets.size(); ++i) { auto iter = std::find(used_filaments.begin(), used_filaments.end(), i + 1); - if (iter == used_filaments.end()) continue; + if (iter == used_filaments.end()) + continue; std::string filament_name = this->filament_presets[i]; for (int f_index = 0; f_index < this->filaments.size(); f_index++) { - PresetCollection *filament_presets = &this->filaments; - Preset *preset = &filament_presets->preset(f_index); - int size = this->filaments.size(); + PresetCollection* filament_presets = &this->filaments; + Preset* preset = &filament_presets->preset(f_index); + int size = this->filaments.size(); if (preset && filament_name.compare(preset->name) == 0) { std::string display_filament_type; std::string filament_type = preset->config.get_filament_type(display_filament_type); @@ -3762,18 +4180,20 @@ void PresetBundle::set_calibrate_printer(std::string name) } if (!name.empty()) calibrate_printer = printers.find_preset(name); - const Preset & printer_preset = calibrate_printer ? *calibrate_printer : printers.get_edited_preset(); + const Preset& printer_preset = calibrate_printer ? *calibrate_printer : printers.get_edited_preset(); const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer_preset); - DynamicPrintConfig config; + DynamicPrintConfig config; config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); - const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); - if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int) static_cast(opt)->values.size())); + const ConfigOption* opt = active_printer.preset.config.option("nozzle_diameter"); + if (opt) + config.set_key_value("num_extruders", new ConfigOptionInt((int) static_cast(opt)->values.size())); calibrate_filaments.clear(); for (size_t i = filaments.num_default_presets(); i < filaments.size(); ++i) { - const Preset & preset = filaments.m_presets[i]; + const Preset& preset = filaments.m_presets[i]; const PresetWithVendorProfile this_preset_with_vendor_profile = filaments.get_preset_with_vendor_profile(preset); - bool is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); - if (is_compatible) calibrate_filaments.insert(&preset); + bool is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); + if (is_compatible) + calibrate_filaments.insert(&preset); } } @@ -3794,25 +4214,31 @@ std::vector> PresetBundle::get_extruder_filament return filament_infos; } -std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only) +std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string& printer_type, + std::string nozzle_diameter_str, + bool system_only) { std::set printer_names; if ("0.0" == nozzle_diameter_str || nozzle_diameter_str.empty()) { nozzle_diameter_str = "0.4"; } - std::ostringstream stream; + std::ostringstream stream; for (auto printer_it = this->printers.begin(); printer_it != this->printers.end(); printer_it++) { - if (system_only && !printer_it->is_system) continue; + if (system_only && !printer_it->is_system) + continue; - ConfigOption * printer_model_opt = printer_it->config.option("printer_model"); - ConfigOptionString *printer_model_str = dynamic_cast(printer_model_opt); - if (!printer_model_str) continue; + ConfigOption* printer_model_opt = printer_it->config.option("printer_model"); + ConfigOptionString* printer_model_str = dynamic_cast(printer_model_opt); + if (!printer_model_str) + continue; // use printer_model as printer type - if (printer_model_str->value != printer_type) continue; + if (printer_model_str->value != printer_type) + continue; - if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) printer_names.insert(printer_it->name); + if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) + printer_names.insert(printer_it->name); } assert(printer_names.size() == 1); @@ -3824,32 +4250,40 @@ std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle return printer_names; } -bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray( - const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id) +bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string& printer_type, + std::string& nozzle_diameter_str, + std::string& setting_id, + std::string& tag_uid, + std::string& nozzle_temp_min, + std::string& nozzle_temp_max, + std::string& preset_setting_id) { bool is_equation = true; - std::map> filament_list = filaments.get_filament_presets(); - std::set printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); + std::map> filament_list = filaments.get_filament_presets(); + std::set printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); - for (const Preset *preset : filament_list.find(setting_id)->second) { - if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) continue; - if (preset && !preset->is_user()) continue; - ConfigOption * printer_opt = const_cast(preset)->config.option("compatible_printers"); - ConfigOptionStrings *printer_strs = dynamic_cast(printer_opt); - bool compared = false; - for (const std::string &printer_str : printer_strs->values) { + for (const Preset* preset : filament_list.find(setting_id)->second) { + if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) + continue; + if (preset && !preset->is_user()) + continue; + ConfigOption* printer_opt = const_cast(preset)->config.option("compatible_printers"); + ConfigOptionStrings* printer_strs = dynamic_cast(printer_opt); + bool compared = false; + for (const std::string& printer_str : printer_strs->values) { if (printer_names.find(printer_str) != printer_names.end()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "nozzle temp matching: preset name: " << preset->name << " printer name: " << printer_str; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "nozzle temp matching: preset name: " << preset->name + << " printer name: " << printer_str; // Compare only once if (!compared) { - compared = true; - bool min_temp_equation = false, max_temp_equation = false; - int min_nozzle_temp = std::stoi(nozzle_temp_min); - int max_nozzle_temp = std::stoi(nozzle_temp_max); - ConfigOption *opt_min = const_cast(preset)->config.option("nozzle_temperature_range_low"); + compared = true; + bool min_temp_equation = false, max_temp_equation = false; + int min_nozzle_temp = std::stoi(nozzle_temp_min); + int max_nozzle_temp = std::stoi(nozzle_temp_max); + ConfigOption* opt_min = const_cast(preset)->config.option("nozzle_temperature_range_low"); if (opt_min) { - ConfigOptionInts *opt_min_ints = dynamic_cast(opt_min); + ConfigOptionInts* opt_min_ints = dynamic_cast(opt_min); min_nozzle_temp = opt_min_ints->get_at(0); if (std::to_string(min_nozzle_temp) == nozzle_temp_min) min_temp_equation = true; @@ -3858,9 +4292,9 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m nozzle_temp_min = std::to_string(min_nozzle_temp); } } - ConfigOption *opt_max = const_cast(preset)->config.option("nozzle_temperature_range_high"); + ConfigOption* opt_max = const_cast(preset)->config.option("nozzle_temperature_range_high"); if (opt_max) { - ConfigOptionInts *opt_max_ints = dynamic_cast(opt_max); + ConfigOptionInts* opt_max_ints = dynamic_cast(opt_max); max_nozzle_temp = opt_max_ints->get_at(0); if (std::to_string(max_nozzle_temp) == nozzle_temp_max) max_temp_equation = true; @@ -3870,11 +4304,13 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m } } if (min_temp_equation && max_temp_equation) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: no changed"; + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: no changed"; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: has changed"; + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: has changed"; preset_setting_id = preset->setting_id; - is_equation = false; + is_equation = false; } } else { assert(false); @@ -3885,7 +4321,7 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m return is_equation; } -Preset *PresetBundle::get_similar_printer_preset(std::string printer_model, std::string printer_variant) +Preset* PresetBundle::get_similar_printer_preset(std::string printer_model, std::string printer_variant) { if (printer_model.empty()) printer_model = printers.get_selected_preset().config.opt_string("printer_model"); @@ -3893,7 +4329,7 @@ Preset *PresetBundle::get_similar_printer_preset(std::string printer_model, std: return nullptr; auto printer_variant_old = printers.get_selected_preset().config.opt_string("printer_variant"); std::map printer_presets; - for (auto &preset : printers.m_presets) { + for (auto& preset : printers.m_presets) { if (printer_variant.empty() && !preset.is_system) continue; if (preset.config.opt_string("printer_model") == printer_model) @@ -3901,7 +4337,8 @@ Preset *PresetBundle::get_similar_printer_preset(std::string printer_model, std: } if (printer_presets.empty()) return nullptr; - auto prefer_printer = printers.get_selected_preset().alias; //.name ORCA use alias instead "name" for calling system presets. otherwise nozzle combo will not change printer presets if they custom named + auto prefer_printer = printers.get_selected_preset().alias; //.name ORCA use alias instead "name" for calling system presets. otherwise + // nozzle combo will not change printer presets if they custom named if (!printer_variant.empty()) boost::replace_all(prefer_printer, printer_variant_old, printer_variant); @@ -3919,30 +4356,29 @@ Preset *PresetBundle::get_similar_printer_preset(std::string printer_model, std: return printer_presets.begin()->second; } -//BBS: check whether this is the only edited filament +// BBS: check whether this is the only edited filament bool PresetBundle::is_the_only_edited_filament(unsigned int filament_index) { unsigned n = this->filament_presets.size(); if (filament_index >= n) return false; - std::string name = this->filament_presets[filament_index]; + std::string name = this->filament_presets[filament_index]; Preset& edited_preset = this->filaments.get_edited_preset(); if (edited_preset.name != name) return false; unsigned index = 0; - while (index < n) - { + while (index < n) { if (index == filament_index) { - index ++; + index++; continue; } std::string filament_preset = this->filament_presets[index]; if (edited_preset.name == filament_preset) return false; else - index ++; + index++; } return true; } @@ -3950,7 +4386,8 @@ bool PresetBundle::is_the_only_edited_filament(unsigned int filament_index) void PresetBundle::reset_default_nozzle_volume_type() { Preset& current_printer = this->printers.get_edited_preset(); - this->project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; + this->project_config.option("nozzle_volume_type")->values = + current_printer.config.option("default_nozzle_volume_type")->values; } int PresetBundle::get_printer_extruder_count() const @@ -3979,9 +4416,7 @@ void PresetBundle::update_filament_count() const size_t num_extruders = static_cast(get_printer_extruder_count()); if (filament_presets.size() >= num_extruders) return; - filament_presets.resize(num_extruders, filament_presets.empty() - ? filaments.first_visible().name - : filament_presets.back()); + filament_presets.resize(num_extruders, filament_presets.empty() ? filaments.first_visible().name : filament_presets.back()); } bool PresetBundle::support_different_extruders() const @@ -4000,8 +4435,7 @@ std::vector PresetBundle::get_default_nozzle_volume_types_for_filaments(std result.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); auto opt_nozzle_volume_type = dynamic_cast(this->project_config.option("nozzle_volume_type")); - for (int index = 0; index < filament_count; index++) - { + for (int index = 0; index < filament_count; index++) { if (opt_nozzle_volume_type && opt_nozzle_volume_type->values.size() > (f_maps[index] - 1)) result[index] = opt_nozzle_volume_type->values[f_maps[index] - 1]; } @@ -4009,40 +4443,43 @@ std::vector PresetBundle::get_default_nozzle_volume_types_for_filaments(std return result; } -DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional>filament_maps, std::optional> filament_volume_maps) const +DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, + std::optional> filament_maps, + std::optional> filament_volume_maps) const { return (this->printers.get_edited_preset().printer_technology() == ptFFF) ? - this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) : - this->full_sla_config(); + this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) : + this->full_sla_config(); } -DynamicPrintConfig PresetBundle::full_config_secure(std::optional>filament_maps) const +DynamicPrintConfig PresetBundle::full_config_secure(std::optional> filament_maps) const { DynamicPrintConfig config = this->full_fff_config(false, filament_maps); - //FIXME legacy, the keys should not be there after conversion to a Physical Printer profile. + // FIXME legacy, the keys should not be there after conversion to a Physical Printer profile. config.erase("print_host"); config.erase("print_host_webui"); config.erase("printhost_apikey"); - config.erase("printhost_cafile"); - config.erase("printhost_user"); - config.erase("printhost_password"); + config.erase("printhost_cafile"); + config.erase("printhost_user"); + config.erase("printhost_password"); config.erase("printhost_port"); return config; } std::vector>> PresetBundle::get_full_flush_matrix(bool with_multiplier) const { - auto full_config = this->full_config(); - int extruder_nums = full_config.option("nozzle_diameter")->values.size(); + auto full_config = this->full_config(); + int extruder_nums = full_config.option("nozzle_diameter")->values.size(); std::vector flush_volume_value = full_config.option("flush_volumes_matrix")->values; - int filament_nums = full_config.option("filament_type")->values.size(); + int filament_nums = full_config.option("filament_type")->values.size(); std::vector>> matrix; for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) { - std::vector flush_matrix(cast(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums))); + std::vector flush_matrix(cast(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums))); std::vector> wipe_volumes; for (unsigned int i = 0; i < filament_nums; ++i) - wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums)); + wipe_volumes.push_back( + std::vector(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums)); matrix.emplace_back(wipe_volumes); } @@ -4050,9 +4487,9 @@ std::vector>> PresetBundle::get_full_flush_matrix if (with_multiplier) { // Fast purge mode uses flush_multiplier_fast; the default prime_volume_mode==Default // (or the key absent) reads flush_multiplier, so this is inert. - auto* mode_opt = project_config.option>("prime_volume_mode"); - const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast; - auto* mult_opt = project_config.option(use_fast ? "flush_multiplier_fast" : "flush_multiplier"); + auto* mode_opt = project_config.option>("prime_volume_mode"); + const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast; + auto* mult_opt = project_config.option(use_fast ? "flush_multiplier_fast" : "flush_multiplier"); auto flush_multiplies = mult_opt ? mult_opt->values : project_config.option("flush_multiplier")->values; flush_multiplies.resize(extruder_nums, 1); for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) { @@ -4066,41 +4503,38 @@ std::vector>> PresetBundle::get_full_flush_matrix return matrix; } -const std::set ignore_settings_list ={ - "inherits", - "print_settings_id", "filament_settings_id", "printer_settings_id" -}; +const std::set ignore_settings_list = {"inherits", "print_settings_id", "filament_settings_id", "printer_settings_id"}; -DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional> filament_maps_new, std::optional> filament_volume_maps_new) const +DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, + std::optional> filament_maps_new, + std::optional> filament_volume_maps_new) const { DynamicPrintConfig out; out.apply(FullPrintConfig::defaults()); out.apply(this->prints.get_edited_preset().config); // Add the default filament preset to have the "filament_preset_id" defined. - out.apply(this->filaments.default_preset().config); - out.apply(this->printers.get_edited_preset().config); + out.apply(this->filaments.default_preset().config); + out.apply(this->printers.get_edited_preset().config); out.apply(this->project_config); // BBS - size_t num_filaments = this->filament_presets.size(); + size_t num_filaments = this->filament_presets.size(); std::vector filament_maps = out.option("filament_map")->values; - std::vector filament_volume_maps(num_filaments, (int)nvtStandard); + std::vector filament_volume_maps(num_filaments, (int) nvtStandard); ConfigOptionInts* filament_volume_map_opt = out.option("filament_volume_map"); if (filament_maps_new.has_value()) filament_maps = *filament_maps_new; if (filament_volume_maps_new.has_value()) { - filament_volume_maps = *filament_volume_maps_new; + filament_volume_maps = *filament_volume_maps_new; out.option("filament_volume_map", true)->values = filament_volume_maps; - } - else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments) + } else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments) filament_volume_maps = filament_volume_map_opt->values; - //in some middle state, they may be different + // in some middle state, they may be different if (filament_maps.size() != num_filaments) { filament_maps.resize(num_filaments, 1); - } - else { + } else { assert(filament_maps.size() == num_filaments); } if (filament_volume_maps.size() != num_filaments) { @@ -4114,32 +4548,35 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio std::vector inherits; std::vector filament_ids; std::vector print_compatible_printers; - //BBS: add logic for settings check between different system presets + // BBS: add logic for settings check between different system presets std::vector different_settings; std::string different_print_settings, different_printer_settings; compatible_printers_condition.emplace_back(this->prints.get_edited_preset().compatible_printers_condition()); - const ConfigOptionStrings* compatible_printers = (const_cast(this))->prints.get_edited_preset().config.option("compatible_printers", false); + const ConfigOptionStrings* compatible_printers = + (const_cast(this))->prints.get_edited_preset().config.option("compatible_printers", false); if (compatible_printers) print_compatible_printers = compatible_printers->values; - //BBS: add logic for settings check between different system presets + // BBS: add logic for settings check between different system presets std::string print_inherits = this->prints.get_edited_preset().inherits(); - inherits .emplace_back(print_inherits); - const Preset* print_parent_preset = this->prints.get_selected_preset_parent(); + inherits.emplace_back(print_inherits); + const Preset* print_parent_preset = this->prints.get_selected_preset_parent(); if (print_parent_preset) { - std::vector dirty_options = this->prints.dirty_options_without_option_list(&(this->prints.get_edited_preset()), print_parent_preset, ignore_settings_list, false); + std::vector dirty_options = this->prints.dirty_options_without_option_list(&(this->prints.get_edited_preset()), + print_parent_preset, ignore_settings_list, + false); if (!dirty_options.empty()) { different_print_settings = Slic3r::escape_strings_cstyle(dirty_options); } } different_settings.emplace_back(different_print_settings); - //BBS: update printer config related with variants + // BBS: update printer config related with variants std::vector> nozzle_volume_types; int extruder_count = 1, extruder_volume_type_count = 1; bool different_extruder = false; if (apply_extruder) { - different_extruder = out.support_different_extruders(extruder_count); + different_extruder = out.support_different_extruders(extruder_count); extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); if ((extruder_count > 1) || different_extruder) { @@ -4151,33 +4588,40 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio // per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing // path composes variant_2 first and is unaffected; changing the order here would alter // long-standing composed values, so any fix must re-baseline them. - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); - //update print config related with variants - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); + // update print config related with variants + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + print_options_with_variant, "print_extruder_id", "print_extruder_variant"); } } if (num_filaments <= 1) { - //BBS: update filament config related with variants + // BBS: update filament config related with variants DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config; if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); + filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, + filament_options_with_variant, "", "filament_extruder_variant", 1, + filament_maps[0], (NozzleVolumeType) filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition()); - compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition()); - //BBS: add logic for settings check between different system presets - //std::string filament_inherits = this->filaments.get_edited_preset().inherits(); + compatible_prints_condition.emplace_back(this->filaments.get_edited_preset().compatible_prints_condition()); + // BBS: add logic for settings check between different system presets + // std::string filament_inherits = this->filaments.get_edited_preset().inherits(); std::string current_preset_name = this->filament_presets[0]; - const Preset* preset = this->filaments.find_preset(current_preset_name, true); - std::string filament_inherits = preset->inherits(); - inherits .emplace_back(filament_inherits); + const Preset* preset = this->filaments.find_preset(current_preset_name, true); + std::string filament_inherits = preset->inherits(); + inherits.emplace_back(filament_inherits); filament_ids.emplace_back(this->filaments.get_edited_preset().filament_id); std::string different_filament_settings; - const Preset* filament_parent_preset = this->filaments.get_selected_preset_parent(); + const Preset* filament_parent_preset = this->filaments.get_selected_preset_parent(); if (filament_parent_preset) { - std::vector dirty_options = this->filaments.dirty_options_without_option_list(&(this->filaments.get_edited_preset()), filament_parent_preset, ignore_settings_list, false); + std::vector dirty_options = + this->filaments.dirty_options_without_option_list(&(this->filaments.get_edited_preset()), filament_parent_preset, + ignore_settings_list, false); if (!dirty_options.empty()) { different_filament_settings = Slic3r::escape_strings_cstyle(dirty_options); } @@ -4186,7 +4630,7 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio different_settings.emplace_back(different_filament_settings); std::vector& filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); } else { // Retrieve filament presets and build a single config object for them. @@ -4205,16 +4649,16 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio filament_configs.emplace_back(&(preset->config)); } for (int index = 0; index < num_filaments; index++) { - const DynamicPrintConfig *cfg = filament_configs[index]; - const Preset *preset = filament_presets[index]; + const DynamicPrintConfig* cfg = filament_configs[index]; + const Preset* preset = filament_presets[index]; // The compatible_prints/printers_condition() returns a reference to configuration key, which may not yet exist. - DynamicPrintConfig &cfg_rw = *const_cast(cfg); + DynamicPrintConfig& cfg_rw = *const_cast(cfg); compatible_printers_condition.emplace_back(Preset::compatible_printers_condition(cfg_rw)); - compatible_prints_condition .emplace_back(Preset::compatible_prints_condition(cfg_rw)); + compatible_prints_condition.emplace_back(Preset::compatible_prints_condition(cfg_rw)); - //BBS: add logic for settings check between different system presets + // BBS: add logic for settings check between different system presets std::string filament_inherits = Preset::inherits(cfg_rw); - inherits .emplace_back(filament_inherits); + inherits.emplace_back(filament_inherits); filament_ids.emplace_back(preset->filament_id); std::string different_filament_settings; @@ -4222,15 +4666,13 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio if (preset->is_system || preset->is_default) { bool is_selected = this->filaments.get_selected_preset_name() == preset->name; if (is_selected) { - //use the real preset + // use the real preset filament_parent_preset = const_cast(this)->filaments.find_preset(preset->name, false, true); - } - else { + } else { filament_parent_preset = preset; } - } - else if (!filament_inherits.empty()) - filament_parent_preset = const_cast(this)->filaments.find_preset(filament_inherits, false, true); + } else if (!filament_inherits.empty()) + filament_parent_preset = const_cast(this)->filaments.find_preset(filament_inherits, false, true); if (filament_parent_preset) { std::vector dirty_options = cfg_rw.diff(filament_parent_preset->config); @@ -4239,8 +4681,7 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio while (iter != dirty_options.end()) { if (ignore_settings_list.find(*iter) != ignore_settings_list.end()) { iter = dirty_options.erase(iter); - } - else { + } else { ++iter; } } @@ -4256,19 +4697,22 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); + filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, + nozzle_volume_types, filament_options_with_variant, "", + "filament_extruder_variant", 1, filament_maps[i], + (NozzleVolumeType) filament_volume_maps[i]); } // loop through options and apply them to the resulting config. std::vector filament_variant_count(num_filaments, 1); - for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) { - if (key == "compatible_prints" || key == "compatible_printers") - continue; + for (const t_config_option_key& key : this->filaments.default_preset().config.keys()) { + if (key == "compatible_prints" || key == "compatible_printers") + continue; // Get a destination option. - ConfigOption *opt_dst = out.option(key, false); + ConfigOption* opt_dst = out.option(key, false); if (opt_dst->is_scalar()) { // Get an option, do not create if it does not exist. - const ConfigOption *opt_src = filament_temp_configs.front().option(key); + const ConfigOption* opt_src = filament_temp_configs.front().option(key); if (opt_src != nullptr) opt_dst->set(opt_src); } else { @@ -4281,10 +4725,10 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio for (size_t i = 0; i < filament_opts.size(); ++i) filament_opts[i] = filament_temp_configs[i].option(key); opt_vec_dst->set(filament_opts); - } - else { + } else { for (size_t i = 0; i < num_filaments; ++i) { - const ConfigOptionVectorBase* filament_option = static_cast(filament_temp_configs[i].option(key)); + const ConfigOptionVectorBase* filament_option = static_cast( + filament_temp_configs[i].option(key)); if (i == 0) opt_vec_dst->set(filament_option); else @@ -4299,9 +4743,9 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio } if (!apply_extruder) { - //append filament_self_index + // append filament_self_index std::vector& filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); int k = 0; for (size_t i = 0; i < num_filaments; i++) { @@ -4312,13 +4756,15 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio } } - //BBS: add logic for settings check between different system presets + // BBS: add logic for settings check between different system presets std::string printer_inherits = this->printers.get_edited_preset().inherits(); // Don't store the "compatible_printers_condition" for the printer profile, there is none. - inherits .emplace_back(printer_inherits); - const Preset* printer_parent_preset = this->printers.get_selected_preset_parent(); + inherits.emplace_back(printer_inherits); + const Preset* printer_parent_preset = this->printers.get_selected_preset_parent(); if (printer_parent_preset) { - std::vector dirty_options = this->printers.dirty_options_without_option_list(&(this->printers.get_edited_preset()), printer_parent_preset, ignore_settings_list, false); + std::vector dirty_options = this->printers.dirty_options_without_option_list(&(this->printers.get_edited_preset()), + printer_parent_preset, + ignore_settings_list, false); if (!dirty_options.empty()) { different_printer_settings = Slic3r::escape_strings_cstyle(dirty_options); } @@ -4331,40 +4777,38 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio out.erase("compatible_printers"); out.erase("compatible_printers_condition"); out.erase("inherits"); - //BBS: add logic for settings check between different system presets + // BBS: add logic for settings check between different system presets out.erase("different_settings_to_system"); static const char* keys[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; - for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++ i) { + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { std::string key = std::string(keys[i]); - auto *opt = dynamic_cast(out.option(key, false)); + auto* opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); } - static const char* keys_with_default[] = { - "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", - "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id" - }; - for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++ i) { + static const char* keys_with_default[] = {"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id"}; + for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++i) { std::string key = std::string(keys_with_default[i]); - auto *opt = dynamic_cast(out.option(key, false)); + auto* opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); - if(opt->value < 0 || opt->value > int(num_filaments)) + if (opt->value < 0 || opt->value > int(num_filaments)) opt->value = 0; } - out.option("print_settings_id", true)->value = this->prints.get_selected_preset_name(); + out.option("print_settings_id", true)->value = this->prints.get_selected_preset_name(); out.option("filament_settings_id", true)->values = this->filament_presets; - out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); - out.option("filament_ids", true)->values = filament_ids; - out.option("filament_map", true)->values = filament_maps; + out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); + out.option("filament_ids", true)->values = filament_ids; + out.option("filament_map", true)->values = filament_maps; // Serialize the collected "compatible_printers_condition" and "inherits" fields. // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored. // The vector will not be stored if all fields are empty strings. - auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { + auto add_if_some_non_empty = [&out](std::vector&& values, const std::string& key) { bool nonempty = false; - for (const std::string &v : values) - if (! v.empty()) { + for (const std::string& v : values) + if (!v.empty()) { nonempty = true; break; } @@ -4372,14 +4816,14 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio out.set_key_value(key, new ConfigOptionStrings(std::move(values))); }; add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); - add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); - add_if_some_non_empty(std::move(inherits), "inherits_group"); - //BBS: add logic for settings check between different system presets - add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); - add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); - out.option("extruder_ams_count", true)->values = save_extruder_ams_count_to_string(this->extruder_ams_counts); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); + // BBS: add logic for settings check between different system presets + add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); + add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); + out.option("extruder_ams_count", true)->values = save_extruder_ams_count_to_string(this->extruder_ams_counts); - out.option("printer_technology", true)->value = ptFFF; + out.option("printer_technology", true)->value = ptFFF; return out; } @@ -4391,35 +4835,36 @@ DynamicPrintConfig PresetBundle::full_sla_config() const out.apply(this->sla_materials.get_edited_preset().config); out.apply(this->printers.get_edited_preset().config); // There are no project configuration values as of now, the project_config is reserved for FFF printers. -// out.apply(this->project_config); + // out.apply(this->project_config); - // Collect the "compatible_printers_condition" and "inherits" values over all presets (sla_prints, sla_materials, printers) into a single vector. + // Collect the "compatible_printers_condition" and "inherits" values over all presets (sla_prints, sla_materials, printers) into a + // single vector. std::vector compatible_printers_condition; - std::vector compatible_prints_condition; + std::vector compatible_prints_condition; std::vector inherits; compatible_printers_condition.emplace_back(this->sla_prints.get_edited_preset().compatible_printers_condition()); - inherits .emplace_back(this->sla_prints.get_edited_preset().inherits()); + inherits.emplace_back(this->sla_prints.get_edited_preset().inherits()); compatible_printers_condition.emplace_back(this->sla_materials.get_edited_preset().compatible_printers_condition()); - compatible_prints_condition .emplace_back(this->sla_materials.get_edited_preset().compatible_prints_condition()); - inherits .emplace_back(this->sla_materials.get_edited_preset().inherits()); - inherits .emplace_back(this->printers.get_edited_preset().inherits()); + compatible_prints_condition.emplace_back(this->sla_materials.get_edited_preset().compatible_prints_condition()); + inherits.emplace_back(this->sla_materials.get_edited_preset().inherits()); + inherits.emplace_back(this->printers.get_edited_preset().inherits()); // These two value types clash between the print and filament profiles. They should be renamed. out.erase("compatible_printers"); out.erase("compatible_printers_condition"); out.erase("inherits"); - out.option("sla_print_settings_id", true)->value = this->sla_prints.get_selected_preset_name(); - out.option("sla_material_settings_id", true)->value = this->sla_materials.get_selected_preset_name(); - out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); + out.option("sla_print_settings_id", true)->value = this->sla_prints.get_selected_preset_name(); + out.option("sla_material_settings_id", true)->value = this->sla_materials.get_selected_preset_name(); + out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); // Serialize the collected "compatible_printers_condition" and "inherits" fields. // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored. // The vector will not be stored if all fields are empty strings. - auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { + auto add_if_some_non_empty = [&out](std::vector&& values, const std::string& key) { bool nonempty = false; - for (const std::string &v : values) - if (! v.empty()) { + for (const std::string& v : values) + if (!v.empty()) { nonempty = true; break; } @@ -4427,66 +4872,70 @@ DynamicPrintConfig PresetBundle::full_sla_config() const out.set_key_value(key, new ConfigOptionStrings(std::move(values))); }; add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); - add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); - add_if_some_non_empty(std::move(inherits), "inherits_group"); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); - out.option("printer_technology", true)->value = ptSLA; - return out; + out.option("printer_technology", true)->value = ptSLA; + return out; } // Load an external config file containing the print, filament and printer presets. // Instead of a config file, a G-code may be loaded containing the full set of parameters. // In the future the configuration will likely be read from an AMF file as well. // If the file is loaded successfully, its print / filament / printer profiles will be activated. -ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) +ConfigSubstitutions PresetBundle::load_config_file(const std::string& path, ForwardCompatibilitySubstitutionRule compatibility_rule) { - if (is_gcode_file(path)) { - DynamicPrintConfig config; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, gcodefile %1%, compatibility_rule %2%")%path %compatibility_rule; - config.apply(FullPrintConfig::defaults()); + if (is_gcode_file(path)) { + DynamicPrintConfig config; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(" enter, gcodefile %1%, compatibility_rule %2%") % path % compatibility_rule; + config.apply(FullPrintConfig::defaults()); ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule); Preset::normalize(config); - load_config_file_config(path, true, std::move(config)); - return config_substitutions; - } + load_config_file_config(path, true, std::move(config)); + return config_substitutions; + } - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode") % path; throw Slic3r::RuntimeError(std::string("Unknown configuration file: ") + path); - + return ConfigSubstitutions{}; } +// some filament presets split from one to sperate ones +// following map recording these filament presets +// for example: previously ''Bambu PLA Basic @BBL H2D 0.6 nozzle' was saved in ''Bambu PLA Basic @BBL H2D' with 0.4 +static std::map> filament_preset_convert = + {{"Bambu Lab H2D 0.6 nozzle", + {{"Bambu PLA Basic @BBL H2D", "Bambu PLA Basic @BBL H2D 0.6 nozzle"}, + {"Bambu PLA Matte @BBL H2D", "Bambu PLA Matte @BBL H2D 0.6 nozzle"}, + {"Bambu ABS @BBL H2D", "Bambu ABS @BBL H2D 0.6 nozzle"}}}, + {"Bambu Lab H2D 0.8 nozzle", + {{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"}, + {"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}}}; -//some filament presets split from one to sperate ones -//following map recording these filament presets -//for example: previously ''Bambu PLA Basic @BBL H2D 0.6 nozzle' was saved in ''Bambu PLA Basic @BBL H2D' with 0.4 -static std::map> filament_preset_convert = { -{"Bambu Lab H2D 0.6 nozzle", {{"Bambu PLA Basic @BBL H2D", "Bambu PLA Basic @BBL H2D 0.6 nozzle"}, - {"Bambu PLA Matte @BBL H2D", "Bambu PLA Matte @BBL H2D 0.6 nozzle"}, - {"Bambu ABS @BBL H2D", "Bambu ABS @BBL H2D 0.6 nozzle"}}}, -{"Bambu Lab H2D 0.8 nozzle", {{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"}, - {"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}} -}; - -//convert the old filament preset to new one after split +// convert the old filament preset to new one after split static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name) { auto machine_iter = filament_preset_convert.find(machine_name); - if (machine_iter != filament_preset_convert.end()) - { + if (machine_iter != filament_preset_convert.end()) { std::map& filament_maps = machine_iter->second; - auto filament_iter = filament_maps.find(filament_name); - if (filament_iter != filament_maps.end()) - { + auto filament_iter = filament_maps.find(filament_name); + if (filament_iter != filament_maps.end()) { filament_name = filament_iter->second; } } } // Load a config file from a boost property_tree. This is a private method called from load_config_file. // is_external == false on if called from ConfigWizard -void PresetBundle::load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version, bool selected, PublishedConfig *published_config) +void PresetBundle::load_config_file_config(const std::string& name_or_path, + bool is_external, + DynamicPrintConfig&& config, + Semver file_version, + bool selected, + PublishedConfig* published_config) { PrinterTechnology printer_technology = Preset::printer_technology(config); @@ -4494,8 +4943,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // the author-selected published keys onto the edited presets. const bool is_published = published_config != nullptr && published_config->published; - auto clear_compatible_printers = [](DynamicPrintConfig& config){ - ConfigOption *opt_compatible = config.optptr("compatible_printers"); + auto clear_compatible_printers = [](DynamicPrintConfig& config) { + ConfigOption* opt_compatible = config.optptr("compatible_printers"); if (opt_compatible != nullptr) { assert(opt_compatible->type() == coStrings); if (opt_compatible->type() == coStrings) @@ -4517,44 +4966,49 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool #else // BBS: use filament_colour insteadof filament_settings_id, filament_settings_id sometimes is not generated ConfigOptionStrings* filament_colour_option = config.option("filament_colour"); - size_t num_filaments = filament_colour_option?filament_colour_option->size():0; + size_t num_filaments = filament_colour_option ? filament_colour_option->size() : 0; if (num_filaments == 0) throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); #endif - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": , name_or_path %1%, is_external %2%, num_filaments %3%") % name_or_path % is_external % num_filaments; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(": , name_or_path %1%, is_external %2%, num_filaments %3%") % name_or_path % is_external % + num_filaments; // Make a copy of the "compatible_machine_expression_group" and "inherits_group" vectors, which // accumulate values over all presets (print, filaments, printers). // These values will be distributed into their particular presets when loading. - std::vector compatible_printers_condition_values = std::move(config.option("compatible_machine_expression_group", true)->values); - std::vector compatible_prints_condition_values = std::move(config.option("compatible_process_expression_group", true)->values); - std::vector inherits_values = std::move(config.option("inherits_group", true)->values); - std::vector filament_ids = std::move(config.option("filament_ids", true)->values); - std::vector print_compatible_printers = std::move(config.option("print_compatible_printers", true)->values); - //BBS: add different settings check logic - bool has_different_settings_to_system = config.option("different_settings_to_system")?true:false; - std::vector different_values = std::move(config.option("different_settings_to_system", true)->values); - std::string &compatible_printers_condition = Preset::compatible_printers_condition(config); - std::string &compatible_prints_condition = Preset::compatible_prints_condition(config); - std::string &inherits = Preset::inherits(config); + std::vector compatible_printers_condition_values = std::move( + config.option("compatible_machine_expression_group", true)->values); + std::vector compatible_prints_condition_values = std::move( + config.option("compatible_process_expression_group", true)->values); + std::vector inherits_values = std::move(config.option("inherits_group", true)->values); + std::vector filament_ids = std::move(config.option("filament_ids", true)->values); + std::vector print_compatible_printers = std::move( + config.option("print_compatible_printers", true)->values); + // BBS: add different settings check logic + bool has_different_settings_to_system = config.option("different_settings_to_system") ? true : false; + std::vector different_values = std::move(config.option("different_settings_to_system", true)->values); + std::string& compatible_printers_condition = Preset::compatible_printers_condition(config); + std::string& compatible_prints_condition = Preset::compatible_prints_condition(config); + std::string& inherits = Preset::inherits(config); compatible_printers_condition_values.resize(num_filaments + 2, std::string()); compatible_prints_condition_values.resize(num_filaments, std::string()); inherits_values.resize(num_filaments + 2, std::string()); different_values.resize(num_filaments + 2, std::string()); filament_ids.resize(num_filaments, std::string()); // The "default_filament_profile" will be later extracted into the printer profile. - switch (printer_technology) { - case ptFFF: - config.option("default_print_profile", true); + switch (printer_technology) { + case ptFFF: + config.option("default_print_profile", true); config.option("default_filament_profile", true); - break; - case ptSLA: - config.option("default_sla_print_profile", true); - config.option("default_sla_material_profile", true); - break; + break; + case ptSLA: + config.option("default_sla_print_profile", true); + config.option("default_sla_material_profile", true); + break; default: break; - } + } bool process_multi_extruder = false; std::vector filament_variant_index; @@ -4574,13 +5028,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool filament_extruder_variant.resize(num_filaments, "Direct Drive Standard"); } if (config.option("extruder_variant_list")) { - //3mf support multiple extruder logic - size_t extruder_count = config.option("nozzle_diameter")->values.size(); + // 3mf support multiple extruder logic + size_t extruder_count = config.option("nozzle_diameter")->values.size(); extruder_variant_count = config.option("filament_extruder_variant", true)->size(); - if ((extruder_variant_count != filament_self_indice.size()) - || (extruder_variant_count < num_filaments)) { + if ((extruder_variant_count != filament_self_indice.size()) || (extruder_variant_count < num_filaments)) { assert(false); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path; + BOOST_LOG_TRIVIAL(error) + << __FUNCTION__ + << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % + name_or_path; throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); } if (num_filaments != extruder_variant_count) { @@ -4598,13 +5054,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } } } - //no need to parse extruder_ams_count + // no need to parse extruder_ams_count std::vector extruder_ams_count = std::move(config.option("extruder_ams_count", true)->values); config.erase("extruder_ams_count"); if (this->extruder_ams_counts.empty()) this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count); - // 1) Create a name from the file name. // Keep the suffix (.ini, .gcode, .amf, .3mf etc) to differentiate it from the normal profiles. std::string name = is_external ? boost::filesystem::path(name_or_path).filename().string() : name_or_path; @@ -4612,184 +5067,187 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // 2) If the loading succeeded, split and load the config into print / filament / printer settings. // First load the print and printer presets. - auto load_preset = - [&config, &inherits, &inherits_values, - &compatible_printers_condition, &compatible_printers_condition_values, - &compatible_prints_condition, &compatible_prints_condition_values, - is_external, &name, &name_or_path, file_version, selected] - (PresetCollection &presets, size_t idx, const std::string &key, const std::set &different_keys, std::string filament_id) { - // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles. - inherits = inherits_values[idx]; - compatible_printers_condition = compatible_printers_condition_values[idx]; + auto load_preset = [&config, &inherits, &inherits_values, &compatible_printers_condition, &compatible_printers_condition_values, + &compatible_prints_condition, &compatible_prints_condition_values, is_external, &name, &name_or_path, file_version, + selected](PresetCollection& presets, size_t idx, const std::string& key, + const std::set& different_keys, std::string filament_id) { + // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles. + inherits = inherits_values[idx]; + compatible_printers_condition = compatible_printers_condition_values[idx]; if (idx > 0 && idx - 1 < compatible_prints_condition_values.size()) compatible_prints_condition = compatible_prints_condition_values[idx - 1]; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": , name %1%, is_external %2%, inherits %3%")%name %is_external %inherits; - if (is_external) - presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config, different_keys, PresetCollection::LoadAndSelect::Always, file_version, filament_id); - else + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(": , name %1%, is_external %2%, inherits %3%") % name % is_external % inherits; + if (is_external) + presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config, different_keys, + PresetCollection::LoadAndSelect::Always, file_version, filament_id); + else presets.load_preset(presets.path_from_name(name, inherits.empty()), name, config, selected, file_version).save(nullptr); - }; + }; switch (Preset::printer_technology(config)) { - case ptFFF: - { + case ptFFF: { // A "published" 3MF project keeps the user's currently-selected presets, so the // print / printer / filament presets are NOT loaded from the file. Only the // project config values and the published keys are applied below. if (!is_published) { - //BBS: add different settings logic - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); - std::vector print_different_keys_vector; - std::string print_different_settings = different_values[0]; - Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector); - std::set print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // print_different_keys_set.clear(); - //} - //else + // BBS: add different settings logic + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); + std::vector print_different_keys_vector; + std::string print_different_settings = different_values[0]; + Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector); + std::set print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end()); + // if (!has_different_settings_to_system) { + // print_different_keys_set.clear(); + // } + // else print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); - if (!print_compatible_printers.empty()) { - ConfigOptionStrings* compatible_printers = config.option("compatible_printers", true); - compatible_printers->values = print_compatible_printers; - } + if (!print_compatible_printers.empty()) { + ConfigOptionStrings* compatible_printers = config.option("compatible_printers", true); + compatible_printers->values = print_compatible_printers; + } - load_preset(this->prints, 0, "print_settings_id", print_different_keys_set, std::string()); + load_preset(this->prints, 0, "print_settings_id", print_different_keys_set, std::string()); - //clear compatible printers - clear_compatible_printers(config); + // clear compatible printers + clear_compatible_printers(config); - std::vector printer_different_keys_vector; - std::string printer_different_settings = different_values[num_filaments + 1]; - Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector); - std::set printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // printer_different_keys_set.clear(); - //} - //else + std::vector printer_different_keys_vector; + std::string printer_different_settings = different_values[num_filaments + 1]; + Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector); + std::set printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end()); + // if (!has_different_settings_to_system) { + // printer_different_keys_set.clear(); + // } + // else printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id"); - load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string()); + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id"); + load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string()); - // 3) Now load the filaments. If there are multiple filament presets, split them and load them. - auto old_filament_profile_names = config.option("filament_settings_id", true); - old_filament_profile_names->values.resize(num_filaments, std::string()); + // 3) Now load the filaments. If there are multiple filament presets, split them and load them. + auto old_filament_profile_names = config.option("filament_settings_id", true); + old_filament_profile_names->values.resize(num_filaments, std::string()); - auto old_machine_profile_name = config.option("printer_settings_id", true); + auto old_machine_profile_name = config.option("printer_settings_id", true); - if (num_filaments <= 1) { - // Split the "compatible_printers_condition" and "inherits" values from the cummulative vectors to separate filament presets. - inherits = inherits_values[1]; - compatible_printers_condition = compatible_printers_condition_values[1]; - compatible_prints_condition = compatible_prints_condition_values.front(); - Preset *loaded = nullptr; + if (num_filaments <= 1) { + // Split the "compatible_printers_condition" and "inherits" values from the cummulative vectors to separate filament presets. + inherits = inherits_values[1]; + compatible_printers_condition = compatible_printers_condition_values[1]; + compatible_prints_condition = compatible_prints_condition_values.front(); + Preset* loaded = nullptr; - //BBS: add different settings logic - std::vector filament_different_keys_vector; - std::string filament_different_settings = different_values[1]; - Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); - std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // filament_different_keys_set.clear(); - //} - //else + // BBS: add different settings logic + std::vector filament_different_keys_vector; + std::string filament_different_settings = different_values[1]; + Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); + std::set filament_different_keys_set(filament_different_keys_vector.begin(), + filament_different_keys_vector.end()); + // if (!has_different_settings_to_system) { + // filament_different_keys_set.clear(); + // } + // else filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); - std::string filament_id = filament_ids[0]; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load single filament preset from filament_settings_id"); - if (is_external) { - if (inherits.empty()) - convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values.front()); - else - convert_filament_preset_name(old_machine_profile_name->value, inherits); - loaded = this->filaments.load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config, filament_different_keys_set, PresetCollection::LoadAndSelect::Always, file_version, filament_id).first; - } - else { - // called from Config Wizard. - loaded= &this->filaments.load_preset(this->filaments.path_from_name(name, inherits.empty()), name, config, true, file_version); - loaded->save(nullptr); - } - this->filament_presets.clear(); - this->filament_presets.emplace_back(loaded->name); - } else { - assert(is_external); - // Split the filament presets, load each of them separately. - std::vector configs(num_filaments, this->filaments.default_preset().config); - // loop through options and scatter them into configs. - for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) { - ConfigOption *other_opt = config.option(key); - if (other_opt == nullptr) - continue; - if (other_opt->is_scalar()) { - for (size_t i = 0; i < configs.size(); ++ i) - configs[i].option(key, false)->set(other_opt); + std::string filament_id = filament_ids[0]; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load single filament preset from filament_settings_id"); + if (is_external) { + if (inherits.empty()) + convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values.front()); + else + convert_filament_preset_name(old_machine_profile_name->value, inherits); + loaded = this->filaments + .load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config, + filament_different_keys_set, PresetCollection::LoadAndSelect::Always, file_version, + filament_id) + .first; + } else { + // called from Config Wizard. + loaded = &this->filaments.load_preset(this->filaments.path_from_name(name, inherits.empty()), name, config, true, + file_version); + loaded->save(nullptr); } - else if (key != "compatible_printers" && key != "compatible_prints") { - for (size_t i = 0; i < configs.size(); ++i) { - if (process_multi_extruder && (filament_options_with_variant.find(key) != filament_options_with_variant.end())) { - ConfigOptionVectorBase* other_opt_vec = static_cast(other_opt); - if (other_opt_vec->size() != extruder_variant_count) { - other_opt_vec->resize(extruder_variant_count); - } - size_t next_index = (i < (configs.size() - 1)) ? filament_variant_index[i + 1] : extruder_variant_count; - static_cast(configs[i].option(key, false))->set(other_opt, filament_variant_index[i], next_index - filament_variant_index[i]); + this->filament_presets.clear(); + this->filament_presets.emplace_back(loaded->name); + } else { + assert(is_external); + // Split the filament presets, load each of them separately. + std::vector configs(num_filaments, this->filaments.default_preset().config); + // loop through options and scatter them into configs. + for (const t_config_option_key& key : this->filaments.default_preset().config.keys()) { + ConfigOption* other_opt = config.option(key); + if (other_opt == nullptr) + continue; + if (other_opt->is_scalar()) { + for (size_t i = 0; i < configs.size(); ++i) + configs[i].option(key, false)->set(other_opt); + } else if (key != "compatible_printers" && key != "compatible_prints") { + for (size_t i = 0; i < configs.size(); ++i) { + if (process_multi_extruder && (filament_options_with_variant.find(key) != filament_options_with_variant.end())) { + ConfigOptionVectorBase* other_opt_vec = static_cast(other_opt); + if (other_opt_vec->size() != extruder_variant_count) { + other_opt_vec->resize(extruder_variant_count); + } + size_t next_index = (i < (configs.size() - 1)) ? filament_variant_index[i + 1] : extruder_variant_count; + static_cast(configs[i].option(key, false)) + ->set(other_opt, filament_variant_index[i], next_index - filament_variant_index[i]); + } else + static_cast(configs[i].option(key, false))->set_at(other_opt, 0, i); } - else - static_cast(configs[i].option(key, false))->set_at(other_opt, 0, i); } } - } - // Load the configs into this->filaments and make them active. - this->filament_presets = std::vector(configs.size()); - // To avoid incorrect selection of the first filament preset (means a value of Preset->m_idx_selected) - // in a case when next added preset take a place of previosly selected preset, - // we should add presets from last to first - bool any_modified = false; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load multiple filament preset from filament_settings_id"); - for (int i = (int)configs.size()-1; i >= 0; i--) { - DynamicPrintConfig &cfg = configs[i]; - // Split the "compatible_printers_condition" and "inherits" from the cummulative vectors to separate filament presets. - cfg.opt_string("compatible_printers_condition", true) = compatible_printers_condition_values[i + 1]; - cfg.opt_string("compatible_prints_condition", true) = compatible_prints_condition_values[i]; - cfg.opt_string("inherits", true) = inherits_values[i + 1]; + // Load the configs into this->filaments and make them active. + this->filament_presets = std::vector(configs.size()); + // To avoid incorrect selection of the first filament preset (means a value of Preset->m_idx_selected) + // in a case when next added preset take a place of previosly selected preset, + // we should add presets from last to first + bool any_modified = false; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load multiple filament preset from filament_settings_id"); + for (int i = (int) configs.size() - 1; i >= 0; i--) { + DynamicPrintConfig& cfg = configs[i]; + // Split the "compatible_printers_condition" and "inherits" from the cummulative vectors to separate filament presets. + cfg.opt_string("compatible_printers_condition", true) = compatible_printers_condition_values[i + 1]; + cfg.opt_string("compatible_prints_condition", true) = compatible_prints_condition_values[i]; + cfg.opt_string("inherits", true) = inherits_values[i + 1]; - //BBS: add different settings logic - std::vector filament_different_keys_vector; - std::string filament_different_settings = different_values[i+1]; - Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); - std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // filament_different_keys_set.clear(); - //} - //else + // BBS: add different settings logic + std::vector filament_different_keys_vector; + std::string filament_different_settings = different_values[i + 1]; + Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); + std::set filament_different_keys_set(filament_different_keys_vector.begin(), + filament_different_keys_vector.end()); + // if (!has_different_settings_to_system) { + // filament_different_keys_set.clear(); + // } + // else filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); - std::string filament_id = filament_ids[i]; + std::string filament_id = filament_ids[i]; - // Load all filament presets, but only select the first one in the preset dialog. - std::string& filament_inherit = cfg.opt_string("inherits", true); - if (filament_inherit.empty() && (i < int(old_filament_profile_names->values.size()))) - convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values[i]); - else - convert_filament_preset_name(old_machine_profile_name->value, filament_inherit); - auto [loaded, modified] = this->filaments.load_external_preset(name_or_path, name, - (i < int(old_filament_profile_names->values.size())) ? old_filament_profile_names->values[i] : "", - std::move(cfg), - filament_different_keys_set, - i == 0 ? - PresetCollection::LoadAndSelect::Always : - any_modified ? - PresetCollection::LoadAndSelect::Never : - PresetCollection::LoadAndSelect::OnlyIfModified, - file_version, - filament_id); - any_modified |= modified; - this->filament_presets[i] = loaded->name; + // Load all filament presets, but only select the first one in the preset dialog. + std::string& filament_inherit = cfg.opt_string("inherits", true); + if (filament_inherit.empty() && (i < int(old_filament_profile_names->values.size()))) + convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values[i]); + else + convert_filament_preset_name(old_machine_profile_name->value, filament_inherit); + auto [loaded, modified] = this->filaments.load_external_preset(name_or_path, name, + (i < int(old_filament_profile_names->values.size())) ? + old_filament_profile_names->values[i] : + "", + std::move(cfg), filament_different_keys_set, + i == 0 ? PresetCollection::LoadAndSelect::Always : + any_modified ? + PresetCollection::LoadAndSelect::Never : + PresetCollection::LoadAndSelect::OnlyIfModified, + file_version, filament_id); + any_modified |= modified; + this->filament_presets[i] = loaded->name; + } } - } } // !is_published // Load the project config values. In published mode only the plate/bed geometry keys @@ -4798,19 +5256,17 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool break; } - case ptSLA: - { + case ptSLA: { /*std::set different_keys_set; load_preset(this->sla_prints, 0, "sla_print_settings_id", different_keys_set); load_preset(this->sla_materials, 1, "sla_material_settings_id", different_keys_set); load_preset(this->printers, 2, "printer_settings_id", different_keys_set);*/ break; } - default: - break; + default: break; } - this->update_compatible(PresetSelectCompatibleType::Never); + this->update_compatible(PresetSelectCompatibleType::Never); this->update_multi_material_filament_presets(); // A "published" 3MF project overlays the author-selected published keys onto the user's @@ -4819,7 +5275,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // how to apply) fall through into skipped_keys. if (is_published) { std::vector skipped_keys; - std::set applied_keys; + std::set applied_keys; // Only re-select the edited filament preset when the material overlay changed // something: re-selecting unconditionally would discard the user's unsaved in-memory // filament edits when the published file touches nothing. @@ -4827,35 +5283,33 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Structural keys are never applied (they would rewrite the user's preset // inheritance/structure). Defense-in-depth: a hand-crafted 3MF could list them despite // the dialog, so skip them here too. - const std::set &structural_keys = publish_structural_keys(); + const std::set& structural_keys = publish_structural_keys(); // The printer overlay is restricted to the publishable retraction/z-hop allowlist; // printer-class keys outside it are contract-excluded (never applied, never reported). - const std::set &printer_allowlist = publishable_printer_keys(); - const std::vector &printer_options = Preset::printer_options(); - const std::set printer_option_set(printer_options.begin(), printer_options.end()); - std::set contract_excluded_keys; - auto apply_published = [&](DynamicPrintConfig &target, const std::set *allowlist) { - for (const std::string &key : published_config->published_keys) { + const std::set& printer_allowlist = publishable_printer_keys(); + const std::vector& printer_options = Preset::printer_options(); + const std::set printer_option_set(printer_options.begin(), printer_options.end()); + std::set contract_excluded_keys; + auto apply_published = [&](DynamicPrintConfig& target, const std::set* allowlist) { + for (const std::string& key : published_config->published_keys) { if (applied_keys.count(key) != 0) continue; // already applied // A '#' suffix denotes a variant key; resolve the base key. - const std::string base_key = publish_base_key(key); + const std::string base_key = publish_base_key(key); // Structural keys are never applied (not "skipped due to mismatch"), so bail // out before the applied/skipped bookkeeping. if (structural_keys.count(base_key) != 0) continue; - if (allowlist != nullptr && - printer_option_set.count(base_key) != 0 && - allowlist->count(base_key) == 0) { + if (allowlist != nullptr && printer_option_set.count(base_key) != 0 && allowlist->count(base_key) == 0) { // Printer-class key outside the publishable allowlist: contract-excluded. contract_excluded_keys.insert(base_key); continue; } - const ConfigOption *src_opt = config.option(base_key); + const ConfigOption* src_opt = config.option(base_key); if (src_opt == nullptr) continue; // key not present in the loaded config; record later if (src_opt->is_vector()) { - const ConfigOption *dst_opt = target.option(base_key); + const ConfigOption* dst_opt = target.option(base_key); if (dst_opt == nullptr || !dst_opt->is_vector()) continue; // cannot apply; will be reported as skipped // Type mismatch: ConfigOptionVector::set() throws ConfigurationError on a @@ -4874,8 +5328,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Strict numeric suffix parse: a malformed variant ("#abc", "#1x") // must be reported as skipped, not silently applied as element 0. const std::string suffix = key.substr(base_key.size() + 1); - size_t idx = 0; - bool valid = !suffix.empty(); + size_t idx = 0; + bool valid = !suffix.empty(); for (const char c : suffix) { if (c < '0' || c > '9') { valid = false; @@ -4887,8 +5341,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool break; } } - if (!valid || - idx >= static_cast(src_opt)->size() || + if (!valid || idx >= static_cast(src_opt)->size() || idx >= static_cast(dst_opt)->size()) continue; // malformed or out-of-range variant: cannot apply; reported as skipped } else if (static_cast(src_opt)->size() != @@ -4905,7 +5358,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (key.find('#') != std::string::npos) continue; // Scalar key: apply only if present on the user's machine. - const ConfigOption *dst_opt = target.option(base_key); + const ConfigOption* dst_opt = target.option(base_key); if (dst_opt == nullptr) continue; // not present on the edited preset; will be reported as skipped // Type mismatch: skipped, never thrown (see the vector branch above). @@ -4942,9 +5395,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool { // Grow the receiver's slots only as far as the highest published slot (never // shrink, never pull filler materials for unpublished slots). - bool has_published_entries = false; - size_t grow_target = 0; - for (const PublishedMaterialEntry &entry : published_config->material_keys) { + bool has_published_entries = false; + size_t grow_target = 0; + for (const PublishedMaterialEntry& entry : published_config->material_keys) { has_published_entries = true; if (entry.slot >= 0) grow_target = std::max(grow_target, size_t(entry.slot) + 1); @@ -4958,7 +5411,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Slots carrying published content, steering the initial preset selection of // newly grown slots. std::set published_slots; - for (const PublishedMaterialEntry &entry : published_config->material_keys) + for (const PublishedMaterialEntry& entry : published_config->material_keys) if (entry.slot >= 0) published_slots.insert(entry.slot); // Exact-name resolution of each published slot's preset through the collection's @@ -4968,9 +5421,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // break-on-first behavior); an empty value means "no resolution" and the string // tiers (raw name / trimmed bare name / alias) below take over. std::map exact_name_by_slot; - for (const PublishedMaterialEntry &entry : published_config->material_keys) + for (const PublishedMaterialEntry& entry : published_config->material_keys) if (entry.slot >= 0 && !entry.preset_name.empty() && exact_name_by_slot.count(entry.slot) == 0) - if (const Preset *resolved = this->filaments.find_preset2(entry.preset_name, true)) + if (const Preset* resolved = this->filaments.find_preset2(entry.preset_name, true)) exact_name_by_slot.emplace(entry.slot, resolved->name); auto resolved_name_for = [&exact_name_by_slot](int slot) -> std::string { const auto it = exact_name_by_slot.find(slot); @@ -4990,7 +5443,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // filament_id), then exact filament_id, then vendor+type, then type only (a // type-only pick may surface an unrelated preset, e.g. a different vendor's // PLA). - auto candidate_score = [](const Preset &candidate, const PublishedMaterialEntry &entry, const std::string &resolved_name) -> int { + auto candidate_score = [](const Preset& candidate, const PublishedMaterialEntry& entry, + const std::string& resolved_name) -> int { // Exact preset name: raw and collection-resolved forms both outrank the // fuzzy bare/alias tier by tier value, so a receiver preset literally named // "Generic PLA" (tier 4) can never beat the author's exact @@ -5004,23 +5458,22 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool std::string bare_name = entry.preset_name.substr(0, entry.preset_name.find('@')); boost::trim_right(bare_name); if (!bare_name.empty() && - (candidate.alias == entry.preset_name || - candidate.name == bare_name || candidate.alias == bare_name)) + (candidate.alias == entry.preset_name || candidate.name == bare_name || candidate.alias == bare_name)) return 4; } if (!entry.setting_id.empty() && candidate.setting_id == entry.setting_id) return 3; - const ConfigOptionStrings *types = candidate.config.opt("filament_type"); - const ConfigOptionStrings *vendors = candidate.config.opt("filament_vendor"); - const std::string type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); + const ConfigOptionStrings* types = candidate.config.opt("filament_type"); + const ConfigOptionStrings* vendors = candidate.config.opt("filament_vendor"); + const std::string type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); const std::string vendor = (vendors != nullptr && !vendors->values.empty()) ? vendors->get_at(0) : std::string(); if (!entry.filament_id.empty() && candidate.filament_id == entry.filament_id) return 2; // Tier 0/1 family gate: the explicit type requirement when published as // such, otherwise the entry's own material family - so identity scoring // also applies to entries published without a checked Type row. - const std::string required_type = !entry.publish_type_value.empty() - ? entry.publish_type_value : normalize_filament_type(entry.filament_type); + const std::string required_type = !entry.publish_type_value.empty() ? entry.publish_type_value : + normalize_filament_type(entry.filament_type); if (!required_type.empty() && normalize_filament_type(type) == required_type) { if (!entry.filament_vendor.empty() && vendor == entry.filament_vendor) return 1; @@ -5036,19 +5489,19 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // (exact id, then vendor+type, then type only), searching compatible // presets first and falling back to incompatible ones only when no // compatible candidate exists... - for (const PublishedMaterialEntry &entry : published_config->material_keys) { + for (const PublishedMaterialEntry& entry : published_config->material_keys) { // Scored for every entry with an identity (name / ids / family), // not only when a Type requirement was checked; candidate_score's // family tiers fall back to the entry's own filament_type. if (entry.slot != static_cast(new_slot_idx)) continue; const std::string resolved_name = resolved_name_for(entry.slot); - int best_score = -1; - auto scan = [&](bool compatible_only) -> std::pair { + int best_score = -1; + auto scan = [&](bool compatible_only) -> std::pair { int best_score = -1; std::string best_name; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); + const Preset& candidate = this->filaments.preset(i); if (compatible_only && !candidate.is_compatible) continue; const int score = candidate_score(candidate, entry, resolved_name); @@ -5059,10 +5512,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (score < 3 && (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)) continue; best_score = score; - best_name = candidate.name; + best_name = candidate.name; } } - return { best_score, best_name }; + return {best_score, best_name}; }; const auto [compat_score, compat_name] = scan(true); const auto [any_score, any_name] = scan(false); @@ -5074,32 +5527,33 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool initial_preset = any_name; } if (best_score >= 0) - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF grew slot " << new_slot_idx - << " with " << initial_preset << " (score " << best_score << ", preset_name \"" + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF grew slot " << new_slot_idx << " with " + << initial_preset << " (score " << best_score << ", preset_name \"" << entry.preset_name << "\", type \"" << entry.publish_type_value << "\")"; if (best_score >= 0 && best_score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty())) - published_config->material_replacements.emplace_back( - "slot " + std::to_string(new_slot_idx) + ": " + initial_preset + - " (substitute: no exact material match)"); + published_config->material_replacements.emplace_back("slot " + std::to_string(new_slot_idx) + ": " + + initial_preset + + " (substitute: no exact material match)"); break; } // ...otherwise any visible preset not already used by another slot, // preferring the published material's own family when it is known. if (initial_preset.empty()) { std::string slot_family; - for (const PublishedMaterialEntry &entry : published_config->material_keys) + for (const PublishedMaterialEntry& entry : published_config->material_keys) if (entry.slot == static_cast(new_slot_idx)) { - slot_family = !entry.publish_type_value.empty() - ? entry.publish_type_value : normalize_filament_type(entry.filament_type); + slot_family = !entry.publish_type_value.empty() ? entry.publish_type_value : + normalize_filament_type(entry.filament_type); break; } for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); + const Preset& candidate = this->filaments.preset(i); if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) continue; if (!slot_family.empty()) { - const ConfigOptionStrings *types = candidate.config.opt("filament_type"); - const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); + const ConfigOptionStrings* types = candidate.config.opt("filament_type"); + const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : + std::string(); if (normalize_filament_type(cand_type) != slot_family) continue; } @@ -5111,8 +5565,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (initial_preset.empty()) // Unpublished filler slot, or every visible preset is already used: // repeat the receiver's last preset ("Add one filament" behaviour). - initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name - : this->filament_presets.back(); + initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name : + this->filament_presets.back(); this->filament_presets.emplace_back(initial_preset); used_preset_names.insert(initial_preset); } @@ -5121,31 +5575,30 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // effective preset in place, so a shared preset would leak one slot's published // values into every aliased slot. Slot 0 (the receiver's own material) is never // re-assigned; when no unused candidate exists the aliasing stays (unavoidable). - auto referenced_elsewhere = [&](const std::string &preset_name, size_t except_slot) { + auto referenced_elsewhere = [&](const std::string& preset_name, size_t except_slot) { for (size_t s = 0; s < this->filament_presets.size(); ++s) if (s != except_slot && this->filament_presets[s] == preset_name) return true; return false; }; for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) { - if (published_slots.count(static_cast(slot)) == 0 || - !referenced_elsewhere(this->filament_presets[slot], slot)) + if (published_slots.count(static_cast(slot)) == 0 || !referenced_elsewhere(this->filament_presets[slot], slot)) continue; // Prefer the best distinct candidate for the slot's published material // (exact id, then vendor+type, then type only), compatible presets first... std::string replacement; int best_score = -1; - for (const PublishedMaterialEntry &entry : published_config->material_keys) { + for (const PublishedMaterialEntry& entry : published_config->material_keys) { // Scored for every entry with an identity, not only when a Type // requirement was checked (same as the growth seeding above). if (entry.slot != static_cast(slot)) continue; const std::string resolved_name = resolved_name_for(entry.slot); - auto scan = [&](bool compatible_only) -> std::pair { + auto scan = [&](bool compatible_only) -> std::pair { int best_score = -1; std::string best_name; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); + const Preset& candidate = this->filaments.preset(i); if (compatible_only && !candidate.is_compatible) continue; const int score = candidate_score(candidate, entry, resolved_name); @@ -5157,43 +5610,44 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool continue; if (score > best_score) { best_score = score; - best_name = candidate.name; + best_name = candidate.name; } } - return { best_score, best_name }; + return {best_score, best_name}; }; const auto [compat_score, compat_name] = scan(true); const auto [any_score, any_name] = scan(false); if (compat_score >= 0) { - best_score = compat_score; - replacement = compat_name; + best_score = compat_score; + replacement = compat_name; } else if (any_score >= 0) { - best_score = any_score; - replacement = any_name; + best_score = any_score; + replacement = any_name; } if (best_score >= 0) - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF re-pointed slot " << slot - << " to " << replacement << " (score " << best_score << ", preset_name \"" - << entry.preset_name << "\", type \"" << entry.publish_type_value << "\")"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF re-pointed slot " << slot << " to " << replacement + << " (score " << best_score << ", preset_name \"" << entry.preset_name << "\", type \"" + << entry.publish_type_value << "\")"; break; } // ...otherwise any distinct visible preset not referenced by another slot, // preferring the published material's own family when it is known. if (replacement.empty()) { std::string slot_family; - for (const PublishedMaterialEntry &entry : published_config->material_keys) + for (const PublishedMaterialEntry& entry : published_config->material_keys) if (entry.slot == static_cast(slot)) { - slot_family = !entry.publish_type_value.empty() - ? entry.publish_type_value : normalize_filament_type(entry.filament_type); + slot_family = !entry.publish_type_value.empty() ? entry.publish_type_value : + normalize_filament_type(entry.filament_type); break; } for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); + const Preset& candidate = this->filaments.preset(i); if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1))) continue; if (!slot_family.empty()) { - const ConfigOptionStrings *types = candidate.config.opt("filament_type"); - const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); + const ConfigOptionStrings* types = candidate.config.opt("filament_type"); + const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : + std::string(); if (normalize_filament_type(cand_type) != slot_family) continue; } @@ -5204,23 +5658,22 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (replacement.empty()) continue; // every visible preset is referenced: aliasing is unavoidable const std::string aliased_name = this->filament_presets[slot]; - this->filament_presets[slot] = replacement; - material_applied = true; + this->filament_presets[slot] = replacement; + material_applied = true; // The re-point used to be silent; surface it like the other slot changes. - published_config->material_replacements.emplace_back( - "slot " + std::to_string(slot) + ": " + aliased_name + " -> " + replacement + - " (de-aliased: shared profile)"); + published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + ": " + aliased_name + " -> " + + replacement + " (de-aliased: shared profile)"); } // Grow the per-slot colour/type/map project vectors to the new slot count and // seed the new entries so the slots render with colours instead of blank chips // (mirrors set_num_filaments; existing values are left untouched). - ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour"); - ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour"); - ConfigOptionStrings *proj_colour_type = this->project_config.opt("filament_colour_type"); - ConfigOptionInts *proj_map = this->project_config.opt("filament_map"); - ConfigOptionInts *proj_nozzle_map = this->project_config.opt("filament_nozzle_map"); - ConfigOptionInts *proj_volume_map = this->project_config.opt("filament_volume_map"); - const size_t old_colour_count = (proj_colour != nullptr) ? proj_colour->values.size() : 0; + ConfigOptionStrings* proj_colour = this->project_config.opt("filament_colour"); + ConfigOptionStrings* proj_multi_colour = this->project_config.opt("filament_multi_colour"); + ConfigOptionStrings* proj_colour_type = this->project_config.opt("filament_colour_type"); + ConfigOptionInts* proj_map = this->project_config.opt("filament_map"); + ConfigOptionInts* proj_nozzle_map = this->project_config.opt("filament_nozzle_map"); + ConfigOptionInts* proj_volume_map = this->project_config.opt("filament_volume_map"); + const size_t old_colour_count = (proj_colour != nullptr) ? proj_colour->values.size() : 0; // Grow-only: an already-larger project vector is left untouched (the receiver's // slot count never shrinks below its own setup). if (proj_colour && proj_colour->values.size() < target_slots) @@ -5239,22 +5692,22 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool this->ams_multi_color_filment.resize(target_slots); for (size_t slot = old_colour_count; slot < target_slots; ++slot) { std::string seed; - for (const PublishedMaterialEntry &entry : published_config->material_keys) + for (const PublishedMaterialEntry& entry : published_config->material_keys) if (entry.slot == static_cast(slot) && entry.publish_color && !entry.color.empty()) { seed = entry.color; break; } if (seed.empty()) { - if (const Preset *preset = this->filaments.find_preset(this->filament_presets[slot], false)) { - const ConfigOptionStrings *colours = preset->config.opt("filament_colour"); + if (const Preset* preset = this->filaments.find_preset(this->filament_presets[slot], false)) { + const ConfigOptionStrings* colours = preset->config.opt("filament_colour"); if (colours != nullptr && !colours->values.empty()) seed = colours->values.front(); } if (seed.empty()) { // Fall back to the option's registered default instead of a // duplicated literal; if the lookup fails the chip stays blank. - if (const ConfigOptionDef *colour_def = print_config_def.get("filament_colour")) - if (const auto *default_colours = dynamic_cast(colour_def->default_value.get())) + if (const ConfigOptionDef* colour_def = print_config_def.get("filament_colour")) + if (const auto* default_colours = dynamic_cast(colour_def->default_value.get())) if (!default_colours->values.empty()) seed = default_colours->values.front(); } @@ -5269,21 +5722,20 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Rebuild the flush volumes for the grown slot count (as set_num_filaments does). this->update_multi_material_filament_presets(); - auto apply_slot_keys = [&](DynamicPrintConfig &preset_config, const std::vector &slot_keys, int author_slot, - const std::string &material_label) { - for (const std::string &key : slot_keys) { + auto apply_slot_keys = [&](DynamicPrintConfig& preset_config, const std::vector& slot_keys, int author_slot, + const std::string& material_label) { + for (const std::string& key : slot_keys) { const std::string base_key = publish_base_key(key); if (structural_keys.count(base_key) != 0) continue; - const ConfigOption *src_opt = config.option(base_key); - if (src_opt == nullptr || !src_opt->is_vector() || - author_slot < 0 || author_slot >= static_cast(static_cast(src_opt)->size())) { + const ConfigOption* src_opt = config.option(base_key); + if (src_opt == nullptr || !src_opt->is_vector() || author_slot < 0 || + author_slot >= static_cast(static_cast(src_opt)->size())) { skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); continue; } - ConfigOption *dst_opt = preset_config.option(base_key); - if (dst_opt == nullptr || !dst_opt->is_vector() || - static_cast(dst_opt)->empty() || + ConfigOption* dst_opt = preset_config.option(base_key); + if (dst_opt == nullptr || !dst_opt->is_vector() || static_cast(dst_opt)->empty() || dst_opt->type() != src_opt->type()) { skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); continue; @@ -5307,25 +5759,26 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // re-pointing slot 0 mid-loop can still invalidate the layer after earlier // slots wrote to it; that compound case is not chased.) const bool edited_survives_load = this->filament_presets.empty() || - this->filament_presets.front() == this->filaments.get_edited_preset().name; + this->filament_presets.front() == this->filaments.get_edited_preset().name; // Full Publish within-load dedup: identical Full materials (same setting_id // + preset_name identity) share one created instance, so an author who // pointed two slots at one preset yields one standalone copy here. std::map published_full_dedup; - for (const PublishedMaterialEntry &entry : published_config->material_keys) { + for (const PublishedMaterialEntry& entry : published_config->material_keys) { if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size()) continue; // out of range: nothing to do for this slot const size_t slot = size_t(entry.slot); // Resolve the stored preset itself (real=true), never the edited snapshot: // find_preset would return &m_edited_preset for the selected slot. The // overlay target below decides between the edited layer and this preset. - Preset *recv = this->filaments.find_preset(this->filament_presets[slot], false, true); + Preset* recv = this->filaments.find_preset(this->filament_presets[slot], false, true); if (recv == nullptr) continue; - const std::string material_label = entry.filament_id.empty() - ? (entry.publish_type_value.empty() ? entry.filament_type : entry.publish_type_value) - : entry.filament_id; + const std::string material_label = entry.filament_id.empty() ? + (entry.publish_type_value.empty() ? entry.filament_type : + entry.publish_type_value) : + entry.filament_id; // Full Publish: always create a standalone detached copy (even on exact // identity match) as a "Preset Inside Project" (project-embedded: lives @@ -5349,28 +5802,25 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } else { // Baseline: clone the receiver slot's stored preset config (schema- // complete across versions), then overlay the published full_keys. - DynamicPrintConfig new_cfg = recv != nullptr - ? recv->config : this->filaments.default_preset().config; - for (const std::string &key : entry.full_keys) { + DynamicPrintConfig new_cfg = recv != nullptr ? recv->config : this->filaments.default_preset().config; + for (const std::string& key : entry.full_keys) { const std::string base_key = publish_base_key(key); if (structural_keys.count(base_key) != 0) continue; - const ConfigOption *src_opt = config.option(base_key); - if (src_opt == nullptr || !src_opt->is_vector() || - entry.slot < 0 || + const ConfigOption* src_opt = config.option(base_key); + if (src_opt == nullptr || !src_opt->is_vector() || entry.slot < 0 || entry.slot >= static_cast(static_cast(src_opt)->size())) continue; - ConfigOption *dst_opt = new_cfg.option(base_key); + ConfigOption* dst_opt = new_cfg.option(base_key); if (dst_opt == nullptr || !dst_opt->is_vector() || - static_cast(dst_opt)->empty() || - dst_opt->type() != src_opt->type()) + static_cast(dst_opt)->empty() || dst_opt->type() != src_opt->type()) continue; static_cast(dst_opt)->set_at(src_opt, 0, entry.slot); } // The published colour is authoritative even for Full (the payload's // filament_colour plus the explicit publish_color field). if (entry.publish_color && !entry.color.empty()) { - if (ConfigOptionStrings *col = new_cfg.opt("filament_colour", true)) { + if (ConfigOptionStrings* col = new_cfg.opt("filament_colour", true)) { if (col->values.empty()) col->values.emplace_back(); col->values[0] = entry.color; @@ -5380,30 +5830,32 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Naming: stripped variant tail ("Generic PLA @System" -> "Generic // PLA"), then identity fallbacks; collisions uniquify with // "(Published)" / "(Published N)" inside add_detached_preset. - std::string base_name = entry.preset_name.empty() ? std::string() : publish_material_base_name(entry.preset_name); + std::string base_name = entry.preset_name.empty() ? std::string() : + publish_material_base_name(entry.preset_name); if (base_name.empty()) { - base_name = !entry.filament_id.empty() ? entry.filament_id - : (!entry.publish_type_value.empty() ? entry.publish_type_value : entry.filament_type); + base_name = !entry.filament_id.empty() ? + entry.filament_id : + (!entry.publish_type_value.empty() ? entry.publish_type_value : entry.filament_type); if (base_name.empty()) base_name = "Published Filament"; } new_name = this->filaments.add_detached_preset(base_name, std::move(new_cfg), entry.filament_id); published_full_dedup.emplace(dedup_key, new_name); } - const std::string old_name = this->filament_presets[slot]; + const std::string old_name = this->filament_presets[slot]; this->filament_presets[slot] = new_name; - material_applied = true; - published_config->material_replacements.emplace_back( - "slot " + std::to_string(slot) + ": " + old_name + " -> " + new_name + - " (published material imported)"); + material_applied = true; + published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + ": " + old_name + " -> " + + new_name + " (published material imported)"); // Colour is slot-scoped and project-visible: sync into project_config // (the copy already baked it, this makes the chips render). if (entry.publish_color && !entry.color.empty()) { - if (ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour")) { + if (ConfigOptionStrings* proj_colour = this->project_config.opt("filament_colour")) { if (slot < proj_colour->values.size()) proj_colour->values[slot] = entry.color; } - if (ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour")) { + if (ConfigOptionStrings* proj_multi_colour = this->project_config.opt( + "filament_multi_colour")) { if (slot < proj_multi_colour->values.size()) proj_multi_colour->values[slot] = entry.color; } @@ -5411,14 +5863,14 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Fall back to the copy's colour so the chip is never blank: try // the newly created preset's filament_colour, then the option default. std::string seed; - if (const Preset *created = this->filaments.find_preset(new_name, false, true)) { - if (const ConfigOptionStrings *cols = created->config.opt("filament_colour")) + if (const Preset* created = this->filaments.find_preset(new_name, false, true)) { + if (const ConfigOptionStrings* cols = created->config.opt("filament_colour")) if (!cols->values.empty()) seed = cols->values.front(); } if (seed.empty()) { - if (const ConfigOptionDef *colour_def = print_config_def.get("filament_colour")) - if (const auto *defaults = dynamic_cast(colour_def->default_value.get())) + if (const ConfigOptionDef* colour_def = print_config_def.get("filament_colour")) + if (const auto* defaults = dynamic_cast(colour_def->default_value.get())) if (!defaults->values.empty()) seed = defaults->values.front(); } @@ -5439,13 +5891,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // layer when the slot references the collection's edited preset and that // layer will survive the load (see edited_survives_load below), the stored // preset otherwise. - const DynamicPrintConfig &effective_config = (edited_survives_load && recv->name == this->filaments.get_edited_preset().name) - ? this->filaments.get_edited_preset().config - : recv->config; + const DynamicPrintConfig& effective_config = (edited_survives_load && + recv->name == this->filaments.get_edited_preset().name) ? + this->filaments.get_edited_preset().config : + recv->config; if (entry.publish_type && !entry.publish_type_value.empty()) { // Null-guard: a malformed receiver preset may lack filament_type. - const ConfigOptionStrings *recv_types = effective_config.opt("filament_type"); - const std::string recv_type = (recv_types != nullptr && !recv_types->values.empty()) ? recv_types->get_at(0) : std::string(); + const ConfigOptionStrings* recv_types = effective_config.opt("filament_type"); + const std::string recv_type = (recv_types != nullptr && !recv_types->values.empty()) ? recv_types->get_at(0) : + std::string(); if (normalize_filament_type(recv_type) != entry.publish_type_value) { // Type mismatch: replace the slot with the best matching preset, // scored by the published identity (exact preset name, then exact @@ -5461,7 +5915,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool std::string best_name; const std::string resolved_name = resolved_name_for(entry.slot); for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); + const Preset& candidate = this->filaments.preset(i); if (compatible_only && !candidate.is_compatible) continue; const int score = candidate_score(candidate, entry, resolved_name); @@ -5483,34 +5937,34 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool continue; } best_score = score; - best_name = candidate.name; + best_name = candidate.name; } - return { best_score, best_name }; + return {best_score, best_name}; }; - const auto [strict_score, strict_name] = scan(true); + const auto [strict_score, strict_name] = scan(true); const auto [relaxed_score, relaxed_name] = scan(false); if (relaxed_score > strict_score) - return { relaxed_score, relaxed_name }; - return { strict_score, strict_name }; + return {relaxed_score, relaxed_name}; + return {strict_score, strict_name}; }; const auto [compat_score, compat_name] = find_best(true); const auto [any_score, any_name] = find_best(false); - int score = any_score; - std::string replacement = any_name; + int score = any_score; + std::string replacement = any_name; if (compat_score >= 0) { score = compat_score; replacement = compat_name; } if (!replacement.empty()) { - const std::string old_name = recv->name; + const std::string old_name = recv->name; this->filament_presets[slot] = replacement; - recv = this->filaments.find_preset(replacement, false, true); - material_applied = true; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF slot " << slot << " material " << old_name - << " -> " << replacement << " (score " << score << ", preset_name \"" - << entry.preset_name << "\", filament_id \"" << entry.filament_id - << "\", setting_id \"" << entry.setting_id << "\", type \"" - << entry.publish_type_value << "\")"; + recv = this->filaments.find_preset(replacement, false, true); + material_applied = true; + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << ": published 3MF slot " << slot << " material " << old_name << " -> " << replacement + << " (score " << score << ", preset_name \"" << entry.preset_name << "\", filament_id \"" + << entry.filament_id << "\", setting_id \"" << entry.setting_id << "\", type \"" + << entry.publish_type_value << "\")"; std::string replacement_line = "slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement; // A pick that is not the exact published material is a substitute; // an entry without identity fields cannot be judged, so it stays plain. @@ -5521,7 +5975,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Partial publish with no replacement: keep the receiver's // material and report the slot's keys as skipped. (Full entries // never reach this gate: they detach above.) - for (const std::string &key : entry.keys) + for (const std::string& key : entry.keys) skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); apply_slot = false; } @@ -5534,9 +5988,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // preserving the user's unsaved edits), otherwise the stored preset it ended // up on (original or partial type replacement), in place. Full entries never // get here - they detach above. - DynamicPrintConfig &write_config = (edited_survives_load && recv->name == this->filaments.get_edited_preset().name) - ? this->filaments.get_edited_preset().config - : recv->config; + DynamicPrintConfig& write_config = (edited_survives_load && recv->name == this->filaments.get_edited_preset().name) ? + this->filaments.get_edited_preset().config : + recv->config; // Colour is slot-scoped and independent of the type gate; it is also synced // into project_config for GUI rendering. @@ -5544,18 +5998,19 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (recv != nullptr) { // Create the key when the target preset lacks it: the colour is a // requirement, not an override. - if (ConfigOptionStrings *colour = write_config.opt("filament_colour", true)) { + if (ConfigOptionStrings* colour = write_config.opt("filament_colour", true)) { if (colour->values.empty()) colour->values.emplace_back(); colour->values[0] = entry.color; - material_applied = true; + material_applied = true; } } - if (ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour")) { + if (ConfigOptionStrings* proj_colour = this->project_config.opt("filament_colour")) { if (slot < proj_colour->values.size()) proj_colour->values[slot] = entry.color; } - if (ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour")) { + if (ConfigOptionStrings* proj_multi_colour = this->project_config.opt( + "filament_multi_colour")) { if (slot < proj_multi_colour->values.size()) proj_multi_colour->values[slot] = entry.color; } @@ -5567,7 +6022,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } } - for (const std::string &key : published_config->published_keys) { + for (const std::string& key : published_config->published_keys) { if (applied_keys.count(key) != 0) continue; const std::string base_key = publish_base_key(key); @@ -5593,22 +6048,23 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool this->filaments.select_preset_by_name(this->filament_presets.front(), false); } - //BBS - //const std::string &physical_printer = config.option("physical_printer_settings_id", true)->value; + // BBS + // const std::string &physical_printer = config.option("physical_printer_settings_id", true)->value; const std::string physical_printer; if (!is_published) { - if (this->printers.get_edited_preset().is_external || physical_printer.empty()) { - this->physical_printers.unselect_printer(); - } else { - // Activate the physical printer profile if possible. - PhysicalPrinter *pp = this->physical_printers.find_printer(physical_printer, true); - if (pp != nullptr && std::find(pp->preset_names.begin(), pp->preset_names.end(), this->printers.get_edited_preset().name) != pp->preset_names.end()) - this->physical_printers.select_printer(pp->name, this->printers.get_edited_preset().name); - else + if (this->printers.get_edited_preset().is_external || physical_printer.empty()) { this->physical_printers.unselect_printer(); + } else { + // Activate the physical printer profile if possible. + PhysicalPrinter* pp = this->physical_printers.find_printer(physical_printer, true); + if (pp != nullptr && std::find(pp->preset_names.begin(), pp->preset_names.end(), this->printers.get_edited_preset().name) != + pp->preset_names.end()) + this->physical_printers.select_printer(pp->name, this->printers.get_edited_preset().name); + else + this->physical_printers.unselect_printer(); + } } - } - //BBS: add config related logs + // BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); } @@ -5619,27 +6075,31 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // (config_maps) or against base_bundle's filament library, flattens, validates // and registers the preset. Returns the reason loading failed, empty on // success. -std::string PresetBundle::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& config_maps, std::map& filament_id_maps, - PresetCollection* presets_collection, size_t& count, bool is_from_lib, - const std::set* retain_configs) +std::string PresetBundle::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& config_maps, + std::map& filament_id_maps, + PresetCollection* presets_collection, + size_t& count, + bool is_from_lib, + const std::set* retain_configs) { - const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); - const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; - const std::string& preset_name = entry.name; - std::string alias_name, filament_id = entry.filament_id; - std::vector renamed_from = entry.renamed_from; - DynamicPrintConfig config; + const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); + const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; + const std::string& preset_name = entry.name; + std::string alias_name, filament_id = entry.filament_id; + std::vector renamed_from = entry.renamed_from; + DynamicPrintConfig config; const DynamicPrintConfig* default_config = nullptr; - std::string reason; + std::string reason; - //check whether it inherits other preset or not - if (! entry.inherits.empty()) { + // check whether it inherits other preset or not + if (!entry.inherits.empty()) { auto it2 = config_maps.find(entry.inherits); if (it2 != config_maps.end()) default_config = &(it2->second); @@ -5661,16 +6121,14 @@ std::string PresetBundle::load_vendor_preset( } } } - } - else { + } else { ++m_errors; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << preset_name; // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); reason = "Can not find inherits: " + entry.inherits; return reason; } - } - else { + } else { if (presets_collection->type() == Preset::TYPE_PRINTER) default_config = &presets_collection->default_preset_for(entry.config_src).config; else @@ -5695,7 +6153,7 @@ std::string PresetBundle::load_vendor_preset( return reason; } if (config.has("alias")) - alias_name = (dynamic_cast(config.option("alias")))->value; + alias_name = (dynamic_cast(config.option("alias")))->value; Preset::normalize(config); // Report configuration fields, which are misplaced into a wrong group. @@ -5709,37 +6167,36 @@ std::string PresetBundle::load_vendor_preset( if (presets_collection->type() == Preset::TYPE_PRINTER) { // Filter out printer presets, which are not mentioned in the vendor profile. // These presets are considered not installed. - auto printer_model = config.opt_string("printer_model"); + auto printer_model = config.opt_string("printer_model"); if (printer_model.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer model, it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name + << "\" defines no printer model, it will be ignored."; reason = std::string("can not find printer_model"); return reason; } auto printer_variant = config.opt_string("printer_variant"); if (printer_variant.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer variant, it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name + << "\" defines no printer variant, it will be ignored."; reason = std::string("can not find printer_variant"); return reason; } auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), - [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } - ); + [&](const VendorProfile::PrinterModel& m) { return m.id == printer_model; }); if (it_model == current_vendor_profile->models.end()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name + << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; reason = std::string("can not find printer model in vendor profile"); return reason; } auto it_variant = it_model->variant(printer_variant); if (it_variant == nullptr) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name + << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; reason = std::string("can not find printer_variant in vendor profile"); return reason; } @@ -5752,71 +6209,74 @@ std::string PresetBundle::load_vendor_preset( // validated, not variant uniqueness. Validation-only so the app keeps loading existing // profiles unchanged. if (validation_mode && entry.instantiation == "true") { - const auto *nd = config.option("nozzle_diameter"); + const auto* nd = config.option("nozzle_diameter"); std::set nozzles, variant_nozzles; if (nd != nullptr) nozzles.insert(nd->values.begin(), nd->values.end()); std::vector variant_tokens; boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+")); bool variant_ok = true; // printer_variant is already guaranteed non-empty above - for (const std::string &tok : variant_tokens) { + for (const std::string& tok : variant_tokens) { size_t consumed = 0; - double d = string_to_double_decimal_point(tok, &consumed); + double d = string_to_double_decimal_point(tok, &consumed); // Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed. - if (consumed == 0) { variant_ok = false; break; } + if (consumed == 0) { + variant_ok = false; + break; + } variant_nozzles.insert(d); } if (!variant_ok || variant_nozzles != nozzles) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has printer_variant \"" << printer_variant << - "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " - "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " - "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " - "nozzle order (e.g. \"0.4+0.6\")."; + BOOST_LOG_TRIVIAL(error) + << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name + << "\" has printer_variant \"" << printer_variant << "\" that does not match its nozzle_diameter \"" + << (nd ? nd->serialize() : std::string()) + << "\". " + "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " + "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " + "nozzle order (e.g. \"0.4+0.6\")."; } } } - const Preset *preset_existing = presets_collection->find_preset(preset_name, false); + const Preset* preset_existing = presets_collection->find_preset(preset_name, false); if (preset_existing != nullptr) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has already been loaded from another Config Bundle."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name + << "\" has already been loaded from another Config Bundle."; reason = std::string("duplicated defines"); return reason; } - auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / entry.sub_path).make_preferred(); - if(validation_mode) + auto file_path = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / vendor_name / entry.sub_path).make_preferred(); + if (validation_mode) file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred(); // Load the preset into the list of presets, save it to disk. - Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); + Preset& loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { - loaded.is_system = true; - loaded.vendor = current_vendor_profile; - loaded.version = current_vendor_profile->config_version; + loaded.is_system = true; + loaded.vendor = current_vendor_profile; + loaded.version = current_vendor_profile->config_version; loaded.description = entry.description; - loaded.setting_id = entry.setting_id; + loaded.setting_id = entry.setting_id; // Derive the preset setting_id on the fly when a profile ships without one, // matching scripts/assign_vendor_setting_ids.py. Only instantiated presets // carry an id; non-instantiated base profiles return earlier above. This never // touches the per-user cloud-sync setting_id written into user .info files. if (loaded.setting_id.empty() && entry.instantiation == "true") - loaded.setting_id = generate_preset_setting_id( - vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); - loaded.filament_id = filament_id; + loaded.setting_id = generate_preset_setting_id(vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); + loaded.filament_id = filament_id; loaded.m_from_orca_filament_lib = is_from_lib; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; if (presets_collection->type() == Preset::TYPE_FILAMENT) { if (filament_id.empty() && "Template" != vendor_name) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; - //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find filament_id for " << preset_name; + // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); reason = "Can not find filament_id for " + preset_name; return reason; - } - else { + } else { filament_id_maps.emplace(preset_name, filament_id); } } @@ -5840,31 +6300,34 @@ std::string PresetBundle::load_vendor_preset( filaments.set_printer_hold_alias(loaded.alias, loaded); } loaded.renamed_from = std::move(renamed_from); - if (! substitution_context.empty()) - substitutions.push_back({ - preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, - std::string(), std::move(substitution_context.substitutions) }); + if (!substitution_context.empty()) + substitutions.push_back({preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, std::string(), + std::move(substitution_context.substitutions)}); if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) config_maps.emplace(preset_name, loaded.config); ++count; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%") % loaded.name % subfile; return reason; } -//BBS: Load a config bundle file from json +// BBS: Load a config bundle file from json std::pair PresetBundle::load_vendor_configs_from_json( - const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) + const std::string& dir, + const std::string& vendor_name, + LoadConfigBundleAttributes flags, + ForwardCompatibilitySubstitutionRule compatibility_rule, + const PresetBundle* base_bundle) { // Enable substitutions for user config bundle, throw an exception when loading a system profile. - ConfigSubstitutionContext substitution_context { compatibility_rule }; + ConfigSubstitutionContext substitution_context{compatibility_rule}; PresetsConfigSubstitutions substitutions; // Errors already on this bundle when the load began; the cache stamp below // counts only what this parse adds. const int errors_at_entry = m_errors; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%dir.c_str()%compatibility_rule; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%") % dir.c_str() % compatibility_rule; if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) // Reset this bundle, delete user profile files if SaveImported. this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); @@ -5872,13 +6335,15 @@ std::pair PresetBundle::load_vendor_configs_ // Orca: only a whole-vendor load has a cache — the vendor-only and filament-only // scans want a slice of one. Validation reads the JSONs whatever is cached. const boost::filesystem::path dir_path(dir); - const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); - if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { + const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && !flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + if (cacheable && !validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { size_t presets_loaded = 0; - for (const PresetCollection* coll : std::initializer_list{ - &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) + for (const PresetCollection* coll : + std::initializer_list{&this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, + &this->printers}) presets_loaded += coll->m_presets.size() - coll->m_num_default_presets; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", %1% served from its preset cache, %2% presets")%vendor_name%presets_loaded; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(", %1% served from its preset cache, %2% presets") % vendor_name % presets_loaded; return std::make_pair(std::move(substitutions), presets_loaded); } @@ -5902,10 +6367,9 @@ std::pair PresetBundle::load_vendor_configs_ } else if (boost::iequals(iter2.key(), BBL_JSON_KEY_SUB_PATH)) { subpath = iter2.value(); } - } - else { + } else { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": invalid value type for " << iter2.key(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid value type for " << iter2.key(); } } if (!name.empty() && !subpath.empty()) @@ -5927,55 +6391,51 @@ std::pair PresetBundle::load_vendor_configs_ boost::nowide::ifstream ifs(root_file); json j; ifs >> j; - //parse the json elements + // parse the json elements for (auto it = j.begin(); it != j.end(); it++) { if (boost::iequals(it.key(), BBL_JSON_KEY_VERSION)) { - //get version + // get version std::string version_str = it.value(); - auto config_version = Semver::parse(version_str); - if (! config_version) { + auto config_version = Semver::parse(version_str); + if (!config_version) { ++m_errors; - throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") - % vendor_name % version_str % dir).str()); + throw ConfigurationError( + (boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") % + vendor_name % version_str % dir) + .str()); } else { vendor_profile.config_version = std::move(*config_version); } - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { - //get url + } else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + // get url vendor_profile.config_update_url = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) { - //get description - BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": parse "< PresetBundle::load_vendor_configs_ process_subfiles.clear(); } - //2) paste the machine model - for (auto& machine_model : machine_model_subfiles) - { + // 2) paste the machine model + for (auto& machine_model : machine_model_subfiles) { std::string subfile = dir + "/" + vendor_name + "/" + machine_model.second; VendorProfile::PrinterModel model; model.id = machine_model.first; @@ -5994,49 +6453,44 @@ std::pair PresetBundle::load_vendor_configs_ boost::nowide::ifstream ifs(subfile); json j; ifs >> j; - //parse the json elements + // parse the json elements for (auto it = j.begin(); it != j.end(); it++) { if (boost::iequals(it.key(), BBL_JSON_KEY_VERSION)) { - //get version - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { - //get url - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { - //get name + // get version + } else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + // get url + } else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { + // get name model.name = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_MODEL_ID)) { - //get model_id + } else if (boost::iequals(it.key(), BBL_JSON_KEY_MODEL_ID)) { + // get model_id model.model_id = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_NOZZLE_DIAMETER)) { - //get nozzle diameter + } else if (boost::iequals(it.key(), BBL_JSON_KEY_NOZZLE_DIAMETER)) { + // get nozzle diameter std::string nozzle_diameters = it.value(); std::vector variants; if (Slic3r::unescape_strings_cstyle(nozzle_diameters, variants)) { - for (const std::string &variant_name : variants) { + for (const std::string& variant_name : variants) { if (model.variant(variant_name) == nullptr) model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name)); } } else { ++m_errors; - BOOST_LOG_TRIVIAL(error)<< __FUNCTION__ << boost::format(": invalid nozzle_diameters %1% for Vendor %1%") % nozzle_diameters % vendor_name; + BOOST_LOG_TRIVIAL(error) + << __FUNCTION__ + << boost::format(": invalid nozzle_diameters %1% for Vendor %1%") % nozzle_diameters % vendor_name; } - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_PRINTER_TECH)) { - //get printer tech + } else if (boost::iequals(it.key(), BBL_JSON_KEY_PRINTER_TECH)) { + // get printer tech if (boost::algorithm::starts_with(it.value(), "SL")) model.technology = ptSLA; else model.technology = ptFFF; - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_FAMILY)) { - //get family + } else if (boost::iequals(it.key(), BBL_JSON_KEY_FAMILY)) { + // get family model.family = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_MODEL)) { - //get bed model + } else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_MODEL)) { + // get bed model model.bed_model = it.value(); } else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME)) { model.bottom_texture_end_name = it.value(); @@ -6048,28 +6502,26 @@ std::pair PresetBundle::load_vendor_configs_ model.bottom_texture_rect_longer = it.value(); } else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) { model.middle_texture_rect = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_IMAGE_BED_TYPE)) { + } else if (boost::iequals(it.key(), BBL_JSON_KEY_IMAGE_BED_TYPE)) { model.image_bed_type = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_TEXTURE)) { - //get bed texture + } else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_TEXTURE)) { + // get bed texture model.bed_texture = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_HOTEND_MODEL)) { + } else if (boost::iequals(it.key(), BBL_JSON_KEY_HOTEND_MODEL)) { model.hotend_model = it.value(); - } - else if (boost::iequals(it.key(), BBL_JSON_KEY_DEFAULT_MATERIALS)) { - //get machine list + } else if (boost::iequals(it.key(), BBL_JSON_KEY_DEFAULT_MATERIALS)) { + // get machine list std::string default_materials_field = it.value(); if (Slic3r::unescape_strings_cstyle(default_materials_field, model.default_materials)) { - Slic3r::sort_remove_duplicates(model.default_materials); - if (! model.default_materials.empty() && model.default_materials.front().empty()) + Slic3r::sort_remove_duplicates(model.default_materials); + if (!model.default_materials.empty() && model.default_materials.front().empty()) // An empty material was inserted into the list of default materials. Remove it. model.default_materials.erase(model.default_materials.begin()); } else { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid default_materials %1% for Vendor %1%") % default_materials_field % vendor_name; + BOOST_LOG_TRIVIAL(error) + << __FUNCTION__ + << boost::format(": invalid default_materials %1% for Vendor %1%") % default_materials_field % vendor_name; } } else if (boost::iequals(it.key(), BBL_JSON_KEY_NOT_SUPPORT_BED_TYPE)) { // get machine list @@ -6081,65 +6533,67 @@ std::pair PresetBundle::load_vendor_configs_ model.not_support_bed_types.erase(model.not_support_bed_types.begin()); } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ - << boost::format(": invalid not_support_bed_types %1% for Vendor %1%") % not_support_bed_type_field % vendor_name; + << boost::format(": invalid not_support_bed_types %1% for Vendor %1%") % + not_support_bed_type_field % vendor_name; } } } - } - catch(nlohmann::detail::parse_error &err) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); - throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") - %subfile %err.what() % dir).str()); + } catch (nlohmann::detail::parse_error& err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << subfile + << " got a nlohmann::detail::parse_error, reason = " << err.what(); + throw ConfigurationError( + (boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") % subfile % + err.what() % dir) + .str()); } - if (! model.id.empty() && ! model.variants.empty()) + if (!model.id.empty() && !model.variants.empty()) vendor_profile.models.push_back(std::move(model)); } - //insert the vendor profile + // insert the vendor profile this->vendors.emplace(vendor_name, vendor_profile); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%")%vendor_profile.name%vendor_profile.id%vendor_profile.config_version.to_string(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%") % vendor_profile.name % + vendor_profile.id % vendor_profile.config_version.to_string(); if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) return std::make_pair(PresetsConfigSubstitutions{}, 0); // 3) paste the process/filament/print configs - PresetCollection *presets = nullptr; - size_t presets_loaded = 0; + PresetCollection* presets = nullptr; + size_t presets_loaded = 0; // Parse one subfile into a source-form entry — everything the JSON states, // nothing resolved. Loading the entry (load_vendor_preset) is the // same code whether the entry was parsed just now or deserialized from the // vendor's cache. - auto parse_subfile = [this, dir, vendor_name]( - ConfigSubstitutionContext& substitution_context, - const std::pair& subfile_iter, - CachedPreset& entry) -> std::string { - + auto parse_subfile = [this, dir, vendor_name](ConfigSubstitutionContext& substitution_context, + const std::pair& subfile_iter, + CachedPreset& entry) -> std::string { std::string subfile = dir + "/" + vendor_name + "/" + subfile_iter.second; std::string reason; try { std::map key_values; substitution_context.substitutions.clear(); - //parse the json elements + // parse the json elements entry.sub_path = subfile_iter.second; entry.config_src.load_from_json(subfile, substitution_context, false, key_values, reason); if (!reason.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": load config file "< PresetBundle::load_vendor_configs_ << "\" contains invalid \"renamed_from\" key, which is being ignored."; } } - } - catch(nlohmann::detail::parse_error &err) { + } catch (nlohmann::detail::parse_error& err) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << subfile + << " got a nlohmann::detail::parse_error, reason = " << err.what(); reason = std::string("json parse error") + err.what(); return reason; } @@ -6191,8 +6645,8 @@ std::pair PresetBundle::load_vendor_configs_ // so the parse_errors stamped into the cache must hold only what a cache load // will not recount. int install_errors = 0; - auto load_subfiles = [&](std::vector>& subfiles, - std::vector& entries, const char* kind, bool is_from_lib = false) { + auto load_subfiles = [&](std::vector>& subfiles, std::vector& entries, + const char* kind, bool is_from_lib = false) { configs.clear(); filament_id_maps.clear(); for (auto& subfile : subfiles) { @@ -6200,17 +6654,19 @@ std::pair PresetBundle::load_vendor_configs_ std::string reason = parse_subfile(substitution_context, subfile, entry); if (reason.empty()) { const int errors_before_install = m_errors; - reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, - substitution_context, substitutions, configs, filament_id_maps, presets, - presets_loaded, is_from_lib); + reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, substitution_context, substitutions, configs, + filament_id_maps, presets, presets_loaded, is_from_lib); install_errors += m_errors - errors_before_install; } if (!reason.empty()) { ++m_errors; - //parse error + // parse error std::string subfile_path = dir + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir).str()); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ + << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; + throw ConfigurationError( + (boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir) + .str()); } if (will_cache) entries.emplace_back(std::move(entry)); @@ -6220,12 +6676,12 @@ std::pair PresetBundle::load_vendor_configs_ // The section order below — process, filaments (with the ORCA-lib map copy), // printers — is mirrored by load_vendor_cache's install loops; keep the two // in lockstep. - //3.1) paste the process + // 3.1) paste the process presets = &this->prints; load_subfiles(process_subfiles, cache_data.process_entries, "process"); - //3.2) paste the filaments - presets = &this->filaments; + // 3.2) paste the filaments + presets = &this->filaments; const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; load_subfiles(filament_subfiles, cache_data.filament_entries, "filament", is_orca_lib); if (is_orca_lib) { @@ -6233,7 +6689,7 @@ std::pair PresetBundle::load_vendor_configs_ m_filament_id_maps = filament_id_maps; } - //3.3) paste the printers + // 3.3) paste the printers presets = &this->printers; load_subfiles(machine_subfiles, cache_data.machine_entries, "printer"); @@ -6242,13 +6698,13 @@ std::pair PresetBundle::load_vendor_configs_ // wrapped would be added to every future load of this vendor. cache_data.parse_errors = uint64_t(std::max(0, m_errors - errors_at_entry - install_errors)); cache_data.vendors = this->vendors; - if (! VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, - vendor_profile.config_version.to_string(), cache_data)) + if (!VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, vendor_profile.config_version.to_string(), + cache_data)) BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name; } - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%")%presets_loaded; + // BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%") % presets_loaded; return std::make_pair(std::move(substitutions), presets_loaded); } @@ -6286,9 +6742,9 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // Now verify if flush_volumes_matrix has proper size (it is used to deduce number of extruders in wipe tower generator): std::vector old_matrix = this->project_config.option("flush_volumes_matrix")->values; - size_t old_nozzle_nums = this->project_config.option("flush_multiplier")->values.size(); + size_t old_nozzle_nums = this->project_config.option("flush_multiplier")->values.size(); size_t old_number_of_filaments = size_t(sqrt(old_matrix.size() / old_nozzle_nums) + EPSILON); - size_t nozzle_nums = get_printer_extruder_count(); + size_t nozzle_nums = get_printer_extruder_count(); if (old_nozzle_nums != nozzle_nums) { std::vector& f_multiplier = this->project_config.option("flush_multiplier")->values; f_multiplier.resize(nozzle_nums, 1.f); @@ -6297,11 +6753,11 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam if (old_matrix.size() != num_filaments * num_filaments * nozzle_nums) { // First verify if purging volumes presets for each extruder matches number of extruders std::vector& filaments = this->project_config.option("flush_volumes_vector")->values; - while (filaments.size() < 2* num_filaments) { - filaments.push_back(filaments.size()>1 ? filaments[0] : 140.); // copy the values from the first extruder - filaments.push_back(filaments.size()>1 ? filaments[1] : 140.); + while (filaments.size() < 2 * num_filaments) { + filaments.push_back(filaments.size() > 1 ? filaments[0] : 140.); // copy the values from the first extruder + filaments.push_back(filaments.size() > 1 ? filaments[1] : 140.); } - while (filaments.size() > 2* num_filaments) { + while (filaments.size() > 2 * num_filaments) { filaments.pop_back(); filaments.pop_back(); } @@ -6319,14 +6775,19 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // for this nozzle slot; otherwise initialize from the per-filament // flush volumes the same way the (i,j) out-of-range branch does. if (nozzle_id < old_nozzle_nums) { - new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = old_matrix[old_i * old_number_of_filaments + old_j + old_matrix_size * nozzle_id]; + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = + old_matrix[old_i * old_number_of_filaments + old_j + old_matrix_size * nozzle_id]; } else { - new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? + 0. : + filaments[2 * i] + filaments[2 * j + 1]); } } } else { for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) { - new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? + 0. : + filaments[2 * i] + filaments[2 * j + 1]); } } } @@ -6339,15 +6800,15 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // Preset::normalize_inherits: target.find_preset(name, false) resolves "renamed_from" recursively // and returns nullptr for unknown names, so we rewrite only on a positive, changed match and leave // user/deleted names untouched. -static void normalize_compatible_field(Preset &preset, const char *field_key, PresetCollection &target) +static void normalize_compatible_field(Preset& preset, const char* field_key, PresetCollection& target) { - auto *opt = preset.config.option(field_key); + auto* opt = preset.config.option(field_key); if (opt == nullptr) return; - for (std::string &name : opt->values) { + for (std::string& name : opt->values) { if (name.empty()) continue; - if (const Preset *resolved = target.find_preset(name, false); resolved != nullptr && resolved->name != name) + if (const Preset* resolved = target.find_preset(name, false); resolved != nullptr && resolved->name != name) name = resolved->name; } } @@ -6364,56 +6825,59 @@ void PresetBundle::normalize_compatible_presets() // references a process preset. System presets are normalized too: a vendor profile can itself // reference a sibling preset by a name that was later renamed, and the rewrite is in-memory only // (system presets are never persisted back to vendor JSON). (begin()/end() skip defaults.) - auto normalize = [this](PresetCollection &holders, PresetCollection *processes) { - for (Preset &p : holders) { + auto normalize = [this](PresetCollection& holders, PresetCollection* processes) { + for (Preset& p : holders) { normalize_compatible_field(p, "compatible_printers", this->printers); if (processes != nullptr) normalize_compatible_field(p, "compatible_prints", *processes); } }; - normalize(this->prints, nullptr); - normalize(this->filaments, &this->prints); - normalize(this->sla_prints, nullptr); + normalize(this->prints, nullptr); + normalize(this->filaments, &this->prints); + normalize(this->sla_prints, nullptr); normalize(this->sla_materials, &this->sla_prints); } -void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible) +void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, + PresetSelectCompatibleType select_other_filament_if_incompatible) { - const Preset &printer_preset = this->printers.get_edited_preset(); - const PresetWithVendorProfile printer_preset_with_vendor_profile = this->printers.get_preset_with_vendor_profile(printer_preset); + const Preset& printer_preset = this->printers.get_edited_preset(); + const PresetWithVendorProfile printer_preset_with_vendor_profile = this->printers.get_preset_with_vendor_profile(printer_preset); class PreferedProfileMatch { public: - PreferedProfileMatch(const std::string &prefered_alias, const std::string &prefered_name) : - m_prefered_alias(prefered_alias), m_prefered_name(prefered_name) {} + PreferedProfileMatch(const std::string& prefered_alias, const std::string& prefered_name) + : m_prefered_alias(prefered_alias), m_prefered_name(prefered_name) + {} - int operator()(const Preset &preset) const + int operator()(const Preset& preset) const { - return - preset.is_default || preset.is_external ? - // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. - 0 : - ! m_prefered_alias.empty() && m_prefered_alias == preset.alias ? - // Matching an alias, always take this preset with priority. - std::numeric_limits::max() : - // Otherwise take the prefered profile, or the first compatible. - preset.name == m_prefered_name; + return preset.is_default || preset.is_external ? + // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. + 0 : + !m_prefered_alias.empty() && m_prefered_alias == preset.alias ? + // Matching an alias, always take this preset with priority. + std::numeric_limits::max() : + // Otherwise take the prefered profile, or the first compatible. + preset.name == m_prefered_name; } private: - const std::string m_prefered_alias; - const std::string &m_prefered_name; + const std::string m_prefered_alias; + const std::string& m_prefered_name; }; // Matching by the layer height in addition. class PreferedPrintProfileMatch : public PreferedProfileMatch { public: - PreferedPrintProfileMatch(const Preset *preset, const std::string &prefered_name) : - PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name), m_prefered_layer_height(preset ? preset->config.opt_float("layer_height") : 0) {} + PreferedPrintProfileMatch(const Preset* preset, const std::string& prefered_name) + : PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name) + , m_prefered_layer_height(preset ? preset->config.opt_float("layer_height") : 0) + {} - int operator()(const Preset &preset) const + int operator()(const Preset& preset) const { // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. if (preset.is_default || preset.is_external) @@ -6437,11 +6901,12 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri class PreferedFilamentProfileMatch : public PreferedProfileMatch { public: - PreferedFilamentProfileMatch(const Preset *preset, const std::string &prefered_name) : - PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name), - m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string()) {} + PreferedFilamentProfileMatch(const Preset* preset, const std::string& prefered_name) + : PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name) + , m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string()) + {} - int operator()(const Preset &preset) const + int operator()(const Preset& preset) const { // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. if (preset.is_default || preset.is_external) @@ -6449,9 +6914,9 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri int match_quality = PreferedProfileMatch::operator()(preset); if (match_quality < std::numeric_limits::max()) { match_quality += 1; - if(preset.is_visible) + if (preset.is_visible) match_quality += 1; - if (! m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) + if (!m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) match_quality *= 10; } return match_quality; @@ -6465,118 +6930,141 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri class PreferedFilamentsProfileMatch { public: - PreferedFilamentsProfileMatch(const Preset *preset, const std::vector &prefered_names) : - m_prefered_alias(preset ? preset->alias : std::string()), - m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string("PLA")), // BBS: default choose PLA + PreferedFilamentsProfileMatch(const Preset* preset, const std::vector& prefered_names) + : m_prefered_alias(preset ? preset->alias : std::string()) + , m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string("PLA")) + , // BBS: default choose PLA m_prefered_names(prefered_names) - {} + {} - int operator()(const Preset &preset) const + int operator()(const Preset& preset) const { // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. if (preset.is_default || preset.is_external || !preset.is_visible) return 0; - if (! m_prefered_alias.empty() && m_prefered_alias == preset.alias) + if (!m_prefered_alias.empty() && m_prefered_alias == preset.alias) // Matching an alias, always take this preset with priority. return std::numeric_limits::max(); int match_quality = (std::find(m_prefered_names.begin(), m_prefered_names.end(), preset.name) != m_prefered_names.end()) + 1; - if (! m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) + if (!m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) match_quality *= 10; return match_quality; } private: - const std::string m_prefered_alias; - const std::string m_prefered_filament_type; - const std::vector &m_prefered_names; + const std::string m_prefered_alias; + const std::string m_prefered_filament_type; + const std::vector& m_prefered_names; }; - BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets enter, select_other_print_if_incompatible %1%, select_other_filament_if_incompatible %2%")%(int)select_other_print_if_incompatible %(int)select_other_filament_if_incompatible; - switch (printer_preset.printer_technology()) { - case ptFFF: - { - assert(printer_preset.config.has("default_print_profile")); - assert(printer_preset.config.has("default_filament_profile")); - const std::vector &prefered_filament_profiles = printer_preset.config.option("default_filament_profile")->values; + BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets enter, select_other_print_if_incompatible %1%, " + "select_other_filament_if_incompatible %2%") % + (int) select_other_print_if_incompatible % (int) select_other_filament_if_incompatible; + switch (printer_preset.printer_technology()) { + case ptFFF: { + assert(printer_preset.config.has("default_print_profile")); + assert(printer_preset.config.has("default_filament_profile")); + const std::vector& prefered_filament_profiles = + printer_preset.config.option("default_filament_profile")->values; this->prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, - PreferedPrintProfileMatch(this->prints.get_selected_idx() == size_t(-1) ? nullptr : &this->prints.get_edited_preset(), printer_preset.config.opt_string("default_print_profile"))); - const PresetWithVendorProfile print_preset_with_vendor_profile = this->prints.get_edited_preset_with_vendor_profile(); + PreferedPrintProfileMatch(this->prints.get_selected_idx() == size_t(-1) ? + nullptr : + &this->prints.get_edited_preset(), + printer_preset.config.opt_string("default_print_profile"))); + const PresetWithVendorProfile print_preset_with_vendor_profile = this->prints.get_edited_preset_with_vendor_profile(); // Remember whether the filament profiles were compatible before updating the filament compatibility. - std::vector filament_preset_was_compatible(this->filament_presets.size(), false); - for (size_t idx = 0; idx < this->filament_presets.size(); ++ idx) { - Preset *preset = this->filaments.find_preset(this->filament_presets[idx], false); + std::vector filament_preset_was_compatible(this->filament_presets.size(), false); + for (size_t idx = 0; idx < this->filament_presets.size(); ++idx) { + Preset* preset = this->filaments.find_preset(this->filament_presets[idx], false); filament_preset_was_compatible[idx] = preset != nullptr && preset->is_compatible; } // First select a first compatible profile for the preset editor. - BOOST_LOG_TRIVIAL(info) << boost::format("prefered filaments: size %1%, previous selected %2%") %prefered_filament_profiles.size() % this->filaments.get_selected_idx(); - if (this->filaments.get_selected_idx() != size_t(-1)) - { + BOOST_LOG_TRIVIAL(info) << boost::format("prefered filaments: size %1%, previous selected %2%") % + prefered_filament_profiles.size() % this->filaments.get_selected_idx(); + if (this->filaments.get_selected_idx() != size_t(-1)) { BOOST_LOG_TRIVIAL(info) << boost::format("previous selected filament: %1%") % this->filaments.get_edited_preset().name; } for (size_t idx = 0; idx < prefered_filament_profiles.size(); ++idx) { BOOST_LOG_TRIVIAL(info) << boost::format("prefered filament: %1%") % prefered_filament_profiles[idx]; } - this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, select_other_filament_if_incompatible, - PreferedFilamentsProfileMatch(this->filaments.get_selected_idx() == size_t(-1) ? nullptr : &this->filaments.get_edited_preset(), prefered_filament_profiles)); + this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, + select_other_filament_if_incompatible, + PreferedFilamentsProfileMatch(this->filaments.get_selected_idx() == size_t(-1) ? + nullptr : + &this->filaments.get_edited_preset(), + prefered_filament_profiles)); if (select_other_filament_if_incompatible != PresetSelectCompatibleType::Never) { // Verify validity of the current filament presets. - const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : + prefered_filament_profiles.front(); if (this->filament_presets.size() == 1) { // The compatible profile should have been already selected for the preset editor. Just use it. - if (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible.front()) - this->filament_presets.front() = this->filaments.get_edited_preset().name; + if (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible.front()) + this->filament_presets.front() = this->filaments.get_edited_preset().name; } else { - for (size_t idx = 0; idx < this->filament_presets.size(); ++ idx) { - std::string &filament_name = this->filament_presets[idx]; - Preset *preset = this->filaments.find_preset(filament_name, false); - if (preset == nullptr || (! preset->is_compatible && (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible[idx]))) + for (size_t idx = 0; idx < this->filament_presets.size(); ++idx) { + std::string& filament_name = this->filament_presets[idx]; + Preset* preset = this->filaments.find_preset(filament_name, false); + if (preset == nullptr || + (!preset->is_compatible && (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || + filament_preset_was_compatible[idx]))) // Pick a compatible profile. If there are prefered_filament_profiles, use them. - filament_name = this->filaments.first_compatible( - PreferedFilamentProfileMatch(preset, - (idx < prefered_filament_profiles.size()) ? prefered_filament_profiles[idx] : prefered_filament_profile)).name; + filament_name = this->filaments + .first_compatible( + PreferedFilamentProfileMatch(preset, (idx < prefered_filament_profiles.size()) ? + prefered_filament_profiles[idx] : + prefered_filament_profile)) + .name; } } } - break; + break; } - case ptSLA: - { - assert(printer_preset.config.has("default_sla_print_profile")); - assert(printer_preset.config.has("default_sla_material_profile")); - this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, - PreferedPrintProfileMatch(this->sla_prints.get_selected_idx() == size_t(-1) ? nullptr : &this->sla_prints.get_edited_preset(), printer_preset.config.opt_string("default_sla_print_profile"))); + case ptSLA: { + assert(printer_preset.config.has("default_sla_print_profile")); + assert(printer_preset.config.has("default_sla_material_profile")); + this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, + PreferedPrintProfileMatch(this->sla_prints.get_selected_idx() == size_t(-1) ? + nullptr : + &this->sla_prints.get_edited_preset(), + printer_preset.config.opt_string("default_sla_print_profile"))); const PresetWithVendorProfile sla_print_preset_with_vendor_profile = this->sla_prints.get_edited_preset_with_vendor_profile(); - this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, select_other_filament_if_incompatible, - PreferedProfileMatch(this->sla_materials.get_selected_idx() == size_t(-1) ? std::string() : this->sla_materials.get_edited_preset().alias, printer_preset.config.opt_string("default_sla_material_profile"))); - break; - } + this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, + select_other_filament_if_incompatible, + PreferedProfileMatch(this->sla_materials.get_selected_idx() == size_t(-1) ? + std::string() : + this->sla_materials.get_edited_preset().alias, + printer_preset.config.opt_string("default_sla_material_profile"))); + break; + } default: break; } BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets exit"); } - -std::vector PresetBundle::export_current_configs(const std::string & path, - std::function override_confirm, - bool include_modify, - bool export_system_settings) +std::vector PresetBundle::export_current_configs(const std::string& path, + std::function override_confirm, + bool include_modify, + bool export_system_settings) { - const Preset &print_preset = include_modify ? prints.get_edited_preset() : prints.get_selected_preset(); - const Preset &printer_preset = include_modify ? printers.get_edited_preset() : printers.get_selected_preset(); - std::set presets { &print_preset, &printer_preset }; - for (auto &f : filament_presets) { + const Preset& print_preset = include_modify ? prints.get_edited_preset() : prints.get_selected_preset(); + const Preset& printer_preset = include_modify ? printers.get_edited_preset() : printers.get_selected_preset(); + std::set presets{&print_preset, &printer_preset}; + for (auto& f : filament_presets) { auto filament_preset = filaments.find_preset(f, include_modify); - if (filament_preset) presets.insert(filament_preset); + if (filament_preset) + presets.insert(filament_preset); } int overwrite = 0; std::vector result; for (auto preset : presets) { - if ((preset->is_system && !export_system_settings) || preset->is_default) + if ((preset->is_system && !export_system_settings) || preset->is_default) continue; std::string file = path + "/" + preset->name + ".json"; - if (overwrite == 0) overwrite = 1; + if (overwrite == 0) + overwrite = 1; if (boost::filesystem::exists(file) && overwrite < 2) { overwrite = override_confirm(preset->name); if (overwrite == 0 || overwrite == 2) @@ -6590,7 +7078,7 @@ std::vector PresetBundle::export_current_configs(const std::string // Set the filament preset name. As the name could come from the UI selection box, // an optional "(modified)" suffix will be removed from the filament name. -void PresetBundle::set_filament_preset(size_t idx, const std::string &name) +void PresetBundle::set_filament_preset(size_t idx, const std::string& name) { if (idx >= filament_presets.size()) { BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: set_filament_preset out of range %1% - %2%") % idx % filament_presets.size(); @@ -6619,7 +7107,7 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const if (!preset.is_system) continue; // It's per design that the Orca Filament Library can have the empty compatible_printers. - if(preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) + if (preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) continue; auto* compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); if (compatible_printers == nullptr || compatible_printers->values.empty()) { @@ -6642,7 +7130,7 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const // VS Code integrated terminal (Cmd/Ctrl+click) and macOS Terminal.app // (Cmd+double-click); quotes or literal spaces break link detection in both, so // the characters that would terminate the URI token are percent-encoded. -static std::string preset_file_uri(const std::string &file) +static std::string preset_file_uri(const std::string& file) { std::string path; try { @@ -6658,7 +7146,7 @@ static std::string preset_file_uri(const std::string &file) case ' ': uri += "%20"; break; case '#': uri += "%23"; break; case '%': uri += "%25"; break; - default: uri += c; + default: uri += c; } } return uri; @@ -6680,34 +7168,34 @@ bool PresetBundle::check_preset_references() const // Resolve one reference (an inherits parent or a compatible_* entry) against its target // collection and log if it is dangling (unknown) or uses a renamed preset's old name. - auto report_ref = [&](const Preset &p, const std::string &name, const PresetCollection &target, - const char *verb, const char *noun) { - const Preset *resolved = target.find_preset(name, false); + auto report_ref = [&](const Preset& p, const std::string& name, const PresetCollection& target, const char* verb, const char* noun) { + const Preset* resolved = target.find_preset(name, false); if (resolved == nullptr) { found = true; BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " unknown " << noun << " \"" << name << "\":\n" << preset_file_uri(p.file); } else if (resolved->name != name) { found = true; - BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " renamed " << noun << " \"" << name - << "\" (now \"" << resolved->name << "\"):\n" << preset_file_uri(p.file); + BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " renamed " << noun << " \"" << name << "\" (now \"" + << resolved->name << "\"):\n" + << preset_file_uri(p.file); } }; - auto check_list = [&](const Preset &p, const char *key, const PresetCollection &target) { - const auto *opt = p.config.option(key); + auto check_list = [&](const Preset& p, const char* key, const PresetCollection& target) { + const auto* opt = p.config.option(key); if (opt == nullptr) return; - for (const std::string &name : opt->values) + for (const std::string& name : opt->values) if (!name.empty()) report_ref(p, name, target, "references", key); }; - auto check_collection = [&](const PresetCollection &holders, const PresetCollection *processes) { - for (const Preset &p : holders) { + auto check_collection = [&](const PresetCollection& holders, const PresetCollection* processes) { + for (const Preset& p : holders) { if (!p.is_system) continue; - if (const std::string &inh = p.inherits(); !inh.empty()) + if (const std::string& inh = p.inherits(); !inh.empty()) report_ref(p, inh, holders, "inherits", "parent"); check_list(p, "compatible_printers", this->printers); if (processes != nullptr) @@ -6717,10 +7205,10 @@ bool PresetBundle::check_preset_references() const // Printers carry no compatible_printers/compatible_prints (those name a printer, so a printer // holding them makes no sense); check_list is a no-op for them, so only their inherits is checked. - check_collection(this->printers, nullptr); - check_collection(this->prints, nullptr); - check_collection(this->filaments, &this->prints); - check_collection(this->sla_prints, nullptr); + check_collection(this->printers, nullptr); + check_collection(this->prints, nullptr); + check_collection(this->filaments, &this->prints); + check_collection(this->sla_prints, nullptr); check_collection(this->sla_materials, &this->sla_prints); return found; @@ -6738,15 +7226,15 @@ bool PresetBundle::check_duplicate_filament_subtypes() const // printer against its own vendor's filaments. A vendor's compatible_printers // only names that vendor's printers, so same-vendor scoping is correctness // preserving and avoids an O(all printers x all filaments) sweep. - std::map> filaments_by_vendor; - for (const auto &preset : filaments) { + std::map> filaments_by_vendor; + for (const auto& preset : filaments) { if (!preset.is_system || preset.filament_id.empty() || preset.vendor == nullptr) continue; filaments_by_vendor[preset.vendor->name].push_back(&preset); } bool found_duplicates = false; - for (const auto &printer : printers) { + for (const auto& printer : printers) { if (!printer.is_system || printer.vendor == nullptr) continue; auto vendor_it = filaments_by_vendor.find(printer.vendor->name); @@ -6755,40 +7243,39 @@ bool PresetBundle::check_duplicate_filament_subtypes() const const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer); // std::map keeps the reported errors in a deterministic (sorted) order. - std::map> by_filament_id; - for (const Preset *fil : vendor_it->second) + std::map> by_filament_id; + for (const Preset* fil : vendor_it->second) if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*fil), active_printer)) by_filament_id[fil->filament_id].push_back(fil); - for (const auto &entry : by_filament_id) { + for (const auto& entry : by_filament_id) { if (entry.second.size() < 2) continue; found_duplicates = true; // List each conflicting preset with a clickable file:// URI on its own // line, so the profile author can jump straight to the files to fix. std::string presets; - for (const Preset *p : entry.second) + for (const Preset* p : entry.second) presets += "\n - " + p->name + "\n " + preset_file_uri(p->file); - BOOST_LOG_TRIVIAL(error) - << "Ambiguous AMS filament match: " << entry.second.size() - << " filament presets share filament_id \"" << entry.first - << "\" and are all compatible with printer \"" << printer.name - << "\". When matching an AMS spool the slicer cannot tell them apart and" - " silently picks whichever loads first." << presets; + BOOST_LOG_TRIVIAL(error) << "Ambiguous AMS filament match: " << entry.second.size() << " filament presets share filament_id \"" + << entry.first << "\" and are all compatible with printer \"" << printer.name + << "\". When matching an AMS spool the slicer cannot tell them apart and" + " silently picks whichever loads first." + << presets; } } // Print the troubleshooting guidance once, not per error, to keep the log readable. if (found_duplicates) BOOST_LOG_TRIVIAL(error) << "\n========================================\n" - << "How to fix \"Ambiguous AMS filament match\" errors: make sure only ONE filament" - " preset with a given filament_id is compatible with each printer. Either" - "\n (a) remove the overlapping printer from a preset's \"compatible_printers\"" - " list (e.g. a '@printer' preset over-claiming a nozzle that already has its own" - " '@printer 0.x nozzle' preset), or" - "\n (b) if these are genuinely different materials, give each its own" - " \"filament_id\" - a common cause is a wrong \"inherits\" pointing at another" - " material's @base preset."; + << "How to fix \"Ambiguous AMS filament match\" errors: make sure only ONE filament" + " preset with a given filament_id is compatible with each printer. Either" + "\n (a) remove the overlapping printer from a preset's \"compatible_printers\"" + " list (e.g. a '@printer' preset over-claiming a nozzle that already has its own" + " '@printer 0.x nozzle' preset), or" + "\n (b) if these are genuinely different materials, give each its own" + " \"filament_id\" - a common cause is a wrong \"inherits\" pointing at another" + " material's @base preset."; return found_duplicates; } @@ -6804,30 +7291,39 @@ bool BundleMetadata::load_from_json(const std::string& path) json j; ifs >> j; - if (j.contains("id")) this->id = j["id"].get(); + if (j.contains("id")) + this->id = j["id"].get(); - if (j.contains("name")) this->name = j["name"].get(); - else if (j.contains("bundle_id")) this->name = j["bundle_id"].get(); // backwards compat w bundle_structure.json + if (j.contains("name")) + this->name = j["name"].get(); + else if (j.contains("bundle_id")) + this->name = j["bundle_id"].get(); // backwards compat w bundle_structure.json - if (j.contains("version")) this->version = j["version"].get(); + if (j.contains("version")) + this->version = j["version"].get(); - if (j.contains("description")) this->description = j["description"].get(); - else if (j.contains("bundle_type")) this->description = j["bundle_type"].get(); // backwards compat w bundle_structure.json + if (j.contains("description")) + this->description = j["description"].get(); + else if (j.contains("bundle_type")) + this->description = j["bundle_type"].get(); // backwards compat w bundle_structure.json - if (j.contains("author")) this->author = j["author"].get(); + if (j.contains("author")) + this->author = j["author"].get(); - if (j.contains("imported_time")) this->imported_time = j["imported_time"].get(); + if (j.contains("imported_time")) + this->imported_time = j["imported_time"].get(); - if (j.contains("updated_time")) this->updated_time = j["updated_time"].get(); + if (j.contains("updated_time")) + this->updated_time = j["updated_time"].get(); - if (j.contains("print_presets")) - this->print_presets = j["print_presets"].get>(); - - if (j.contains("filament_presets")) - this->filament_presets = j["filament_presets"].get>(); - - if (j.contains("printer_presets")) - this->printer_presets = j["printer_presets"].get>(); + if (j.contains("print_presets")) + this->print_presets = j["print_presets"].get>(); + + if (j.contains("filament_presets")) + this->filament_presets = j["filament_presets"].get>(); + + if (j.contains("printer_presets")) + this->printer_presets = j["printer_presets"].get>(); return true; } catch (const std::exception& e) { @@ -6839,27 +7335,26 @@ bool BundleMetadata::load_from_json(const std::string& path) bool BundleMetadata::save_to_json(const std::string& path) const { auto strip_prefix = [](const std::vector& names) { - json arr = json::array(); - for (const auto& name : names) - { - arr.push_back(boost::filesystem::path(name).filename().string()); - std::string test = boost::filesystem::path(name).filename().string(); - } - return arr; - }; + json arr = json::array(); + for (const auto& name : names) { + arr.push_back(boost::filesystem::path(name).filename().string()); + std::string test = boost::filesystem::path(name).filename().string(); + } + return arr; + }; try { json j; - j["id"] = this->id; - j["name"] = this->name; - j["version"] = this->version; - j["description"] = this->description; - j["author"] = this->author; + j["id"] = this->id; + j["name"] = this->name; + j["version"] = this->version; + j["description"] = this->description; + j["author"] = this->author; j["imported_time"] = this->imported_time; - j["updated_time"] = this->updated_time; + j["updated_time"] = this->updated_time; - j["print_presets"] = strip_prefix(this->print_presets); - j["filament_presets"] = strip_prefix(this->filament_presets); - j["printer_presets"] = strip_prefix(this->printer_presets); + j["print_presets"] = strip_prefix(this->print_presets); + j["filament_presets"] = strip_prefix(this->filament_presets); + j["printer_presets"] = strip_prefix(this->printer_presets); boost::nowide::ofstream ofs(path); ofs << j.dump(4); @@ -6880,13 +7375,14 @@ bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const s // of the installation, against nothing, since nothing on disk can then be // newer than it. That state is Semver::inf(), which no real profile carries. const boost::filesystem::path profile = dir / (vendor_name + ".json"); - const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) - : Semver::inf(); + const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) : Semver::inf(); return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, base_bundle); } -bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, - const Semver& expected_vendor_version, const PresetBundle* base_bundle) +bool PresetBundle::load_vendor_cache(const std::string& cache_path, + const std::string& expected_vendor_name, + const Semver& expected_vendor_version, + const PresetBundle* base_bundle) { // What this bundle had counted before the cache was tried. The caller // measures its own parse against this same baseline, so a rejection must @@ -6895,11 +7391,11 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s // Read and validated before this bundle is touched: a rejected file leaves // no state to roll back. VendorCacheData data; - if (! VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) + if (!VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) return false; try { - const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match - this->vendors = std::move(data.vendors); + const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match + this->vendors = std::move(data.vendors); // What the parse counted before install took over; install recounts its // own below, so m_errors comes out as a JSON parse would leave it. @@ -6909,13 +7405,13 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s // them straight after parsing — same code, same order. The substitution // context stays empty (the entries were substituted when they were // parsed), so no substitutions are reported, as before. - ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; + ConfigSubstitutionContext substitution_context{ForwardCompatibilitySubstitutionRule::EnableSilent}; PresetsConfigSubstitutions substitutions; std::map configs; std::map filament_id_maps; const std::string path = boost::filesystem::path(cache_path).parent_path().string(); - size_t count = 0; - auto install_entries = [&](const std::vector& entries, PresetCollection* presets, bool is_from_lib) { + size_t count = 0; + auto install_entries = [&](const std::vector& entries, PresetCollection* presets, bool is_from_lib) { configs.clear(); filament_id_maps.clear(); // Only configs of presets that other entries inherit are ever looked @@ -6924,14 +7420,14 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s // become the m_config_maps other vendors resolve against. std::set inherited; for (const CachedPreset& entry : entries) - if (! entry.inherits.empty()) + if (!entry.inherits.empty()) inherited.insert(entry.inherits); const std::set* retain_configs = is_from_lib ? nullptr : &inherited; for (const CachedPreset& entry : entries) { - const std::string reason = load_vendor_preset(entry, path, vendor_name, - base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions, - configs, filament_id_maps, presets, count, is_from_lib, retain_configs); - if (! reason.empty()) + const std::string reason = load_vendor_preset(entry, path, vendor_name, base_bundle, LoadConfigBundleAttribute::LoadSystem, + substitution_context, substitutions, configs, filament_id_maps, presets, + count, is_from_lib, retain_configs); + if (!reason.empty()) throw std::runtime_error("entry " + entry.name + " failed to install: " + reason); } }; diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index c3c8d0f8b0..f9c4f36a8c 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -517,6 +517,12 @@ public: // Read out the number of extruders from an active printer preset, // update size and content of filament_presets. void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1)); + // Mixed-color filament slots: virtual slots realized from 2-3 physical filaments. + bool is_mixed_filament(size_t idx) const; + std::vector physical_filament_config_indices() const; + // How many slots are mixed. They sit at the tail of the filament list and have no nozzle of + // their own, so any resize driven by the printer's extruder count has to add this on top. + size_t num_mixed_filaments() const; void on_extruders_count_changed(int extruder_count); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 509744abe2..1bc1015477 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -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 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 *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 *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 *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_iterator print_object_instance_sequential_active; std::vector>> layers_to_print = GCode::collect_layers_to_print(*this); std::vector 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> 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 first_layer_used_filaments; std::vector> 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> nozzle_map_per_layer; std::vector> stitched_layer_filaments; print_object_instance_sequential_active = print_object_instances_ordering.begin(); + std::vector 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(-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 *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> objPrintVec; for (const PrintInstance* instance : print_object_instances_ordering) { const ObjectID& print_object_ID = instance->print_object->id(); @@ -3776,6 +3854,14 @@ bool Print::is_dynamic_group_reorder() const const bool enabled = opt && opt->value; if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1) return false; + + // Dynamic regrouping and mixed-color slots are incompatible: a mixed slot is resolved to + // different physical components per layer, so a group assignment made up-front would be wrong. + const auto &is_mixed = m_config.filament_is_mixed.values; + for (unsigned int filament_id : extruders()) { + if (filament_id < is_mixed.size() && is_mixed[filament_id]) + return false; + } return true; } @@ -3999,38 +4085,36 @@ void Print::_make_wipe_tower() return; // Check whether there are any layers in m_tool_ordering, which are marked with has_wipe_tower, - // they print neither object, nor support. These layers are above the raft and below the object, and they - // shall be added to the support layers to be printed. - // see https://github.com/prusa3d/PrusaSlicer/issues/607 + // they print neither object, nor support. Each such layer needs a virtual support layer + // counterpart in m_objects.front() so that GCode::collect_layers_to_print picks it up and the + // wipe tower G-code is actually emitted for that z. Such layers appear in two scenarios: + // - above the raft, between raft top and the first real object layer + // (see https://github.com/prusa3d/PrusaSlicer/issues/607); + // - between two real wipe-tower layers, when one object is fully floating above another and + // the support_top_z_distance / support_bottom_z_distance gap leaves interior z values with + // neither object nor support (continuity fill in ToolOrdering::fill_wipe_tower_partitions). + // The previous implementation only handled the first contiguous run starting at the first + // virtual layer, which made the second scenario silently produce empty wipe-tower layers. { - size_t idx_begin = size_t(-1); - size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size(); - // Find the first wipe tower layer, which does not have a counterpart in an object or a support layer. + auto &support_layers = m_objects.front()->support_layers(); + auto it_layer = support_layers.begin(); + const size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size(); for (size_t i = 0; i < idx_end; ++ i) { - const LayerTools < = m_wipe_tower_data.tool_ordering.layer_tools()[i]; - if (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support) { - idx_begin = i; - break; - } - } - if (idx_begin != size_t(-1)) { - // Find the position in m_objects.first()->support_layers to insert these new support layers. - double wipe_tower_new_layer_print_z_first = m_wipe_tower_data.tool_ordering.layer_tools()[idx_begin].print_z; - auto it_layer = m_objects.front()->support_layers().begin(); - auto it_end = m_objects.front()->support_layers().end(); - for (; it_layer != it_end && (*it_layer)->print_z - EPSILON < wipe_tower_new_layer_print_z_first; ++ it_layer); - // Find the stopper of the sequence of wipe tower layers, which do not have a counterpart in an object or a support layer. - for (size_t i = idx_begin; i < idx_end; ++ i) { - LayerTools < = const_cast(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(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(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b38a0ca058..9e20061501 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -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 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 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 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 &used_mixed_filaments) { + m_slice_used_mixed_filaments = used_mixed_filaments; + } + const std::vector& 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 m_slice_used_filaments; std::vector m_slice_used_filaments_first_layer; + // 0-based mixed (virtual) filament slots actually used on this plate. + std::vector m_slice_used_mixed_filaments; //BBS: plate's origin Vec3d m_origin {0, 0, 0}; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index e2e9bc737d..f03271bf73 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1,6 +1,7 @@ #include "ClipperUtils.hpp" #include "Model.hpp" #include "Print.hpp" +#include "FilamentMixer.hpp" #include #include @@ -886,7 +887,12 @@ bool verify_update_print_object_regions( size_t hash = regions[i]->config_hash(); size_t j = i; for (++ j; j < regions.size() && regions[j]->config_hash() == hash; ++ j) - if (regions[i]->config() == regions[j]->config()) { + // Same config but different gradient_volume_id is intentional (per-part gradient + // splitting) and must NOT be flagged as a merge. When per-part is off all regions + // carry an invalid (default) gradient_volume_id, so the AND condition is always + // true and behavior matches the legacy check. + if (regions[i]->config() == regions[j]->config() + && regions[i]->gradient_volume_id() == regions[j]->gradient_volume_id()) { // Regions were merged. We need to reslice. return false; } @@ -978,7 +984,10 @@ static PrintObjectRegions* generate_print_object_regions( const float xy_contour_compensation, const std::vector &painting_extruders, std::vector &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 &slot_per_part_enabled = {}) { // Reuse the old object or generate a new one. auto out = print_object_regions_old ? std::unique_ptr(print_object_regions_old) : std::make_unique(); @@ -1013,19 +1022,71 @@ static PrintObjectRegions* generate_print_object_regions( update_volume_bboxes(layer_ranges_regions, out->cached_volume_ids, model_volumes, out->trafo_bboxes, is_mm_painted ? 0.f : std::max(0.f, xy_contour_compensation)); std::vector 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(std::move(config), hash, int(all_regions.size()))); + all_regions.emplace_back(std::make_unique(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 per_part_volume_users; + if (!slot_per_part_enabled.empty()) { + per_part_volume_users.assign(slot_per_part_enabled.size(), 0); + for (const ModelVolume *mv : model_volumes) { + if (! mv->is_model_part()) + continue; + const DynamicPrintConfig *range_cfg = layer_ranges_regions.empty() ? nullptr : layer_ranges_regions.front().config; + PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, range_cfg, *mv, num_extruders, variant_index); + for (unsigned int s_1based : { (unsigned int)vol_cfg.outer_wall_filament_id.value, + (unsigned int)vol_cfg.inner_wall_filament_id.value, + (unsigned int)vol_cfg.sparse_infill_filament_id.value, + (unsigned int)vol_cfg.internal_solid_filament_id.value, + (unsigned int)vol_cfg.top_surface_filament_id.value, + (unsigned int)vol_cfg.bottom_surface_filament_id.value }) { + if (s_1based >= 1 + && size_t(s_1based - 1) < slot_per_part_enabled.size() + && slot_per_part_enabled[s_1based - 1]) + ++per_part_volume_users[s_1based - 1]; + } + } + } + auto compute_volume_tag = [&](const PrintRegionConfig &cfg, const ModelVolume &mv) -> ObjectID { + if (per_part_volume_users.empty()) + return ObjectID(); + auto qualifies = [&](unsigned int s_1based) { + return s_1based >= 1 + && size_t(s_1based - 1) < slot_per_part_enabled.size() + && slot_per_part_enabled[s_1based - 1] + && per_part_volume_users[s_1based - 1] >= 2; + }; + if (qualifies((unsigned int)cfg.outer_wall_filament_id.value) + || qualifies((unsigned int)cfg.inner_wall_filament_id.value) + || qualifies((unsigned int)cfg.sparse_infill_filament_id.value) + || qualifies((unsigned int)cfg.internal_solid_filament_id.value) + || qualifies((unsigned int)cfg.top_surface_filament_id.value) + || qualifies((unsigned int)cfg.bottom_surface_filament_id.value)) { + return mv.id(); + } + return ObjectID(); + }; + // Chain the regions in the order they are stored in the volumes list. for (int volume_id = 0; volume_id < int(model_volumes.size()); ++ volume_id) { const ModelVolume &volume = *model_volumes[volume_id]; @@ -1034,9 +1095,11 @@ static PrintObjectRegions* generate_print_object_regions( if (const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, volume); bbox) { if (volume.is_model_part()) { // Add a model volume, assign an existing region or generate a new one. + PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index); + ObjectID volume_tag = compute_volume_tag(vol_cfg, volume); layer_range.volume_regions.push_back({ &volume, -1, - get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index)), + get_create_region(std::move(vol_cfg), volume_tag), bbox }); } else if (volume.is_negative_volume()) { @@ -1121,6 +1184,12 @@ static PrintObjectRegions* generate_print_object_regions( } } + + // Save the slot_per_part_enabled bit vector that produced these regions, so the guard in + // Print::apply can detect changes on the next call even when PrintRegionConfig did not + // change. Always written — including an empty vector — so the snapshot always reflects + // the exact input used to generate the current regions. + out->last_slot_per_part_enabled = slot_per_part_enabled; return out.release(); } @@ -1141,6 +1210,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ std::vector used_filaments = this->extruders(true); std::unordered_set 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("filament_is_mixed"); + auto* comp_strs_opt = new_full_config.option("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + auto expanded = expand_mixed_filaments(used_filaments, is_mixed_opt->values, comp_strs_opt->values); + used_filament_set.insert(expanded.begin(), expanded.end()); + } + } + //new_full_config.normalize_fdm(used_filaments); new_full_config.normalize_fdm_1(); t_config_option_keys changed_keys = new_full_config.normalize_fdm_2(objects().size(), used_filaments.size()); @@ -1802,6 +1882,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_filament_self_index_cache(); } + // Per-part gradient: compute the per-slot enable bit vector once for this Print::apply pass. + // Used by generate_print_object_regions to decide which volumes deserve their own PrintRegion. + std::vector 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 comps = parse_mixed_components(i < components_vec.size() ? components_vec[i] : ""); + if (comps.size() != 2) + continue; + if (i >= grad_vec.size() || ! grad_vec[i]) + continue; + if (i >= per_part_vec.size() || ! per_part_vec[i]) + continue; + slot_per_part_enabled[i] = true; + } + } + // All regions now have distinct settings. // Check whether applying the new region config defaults we would get different regions, // update regions or create regions from scratch. @@ -1828,7 +1931,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ for (const ModelVolume *volume : volumes) { const std::vector &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states; - assert(volume_used_facet_states.size() == used_facet_states.size()); + // Paint data saved before the painted state range was extended deserializes a + // shorter used_states vector, so merge over the common prefix. for (size_t state_idx = 0; state_idx < std::min(volume_used_facet_states.size(), used_facet_states.size()); ++state_idx) used_facet_states[state_idx] |= volume_used_facet_states[state_idx]; } @@ -1862,6 +1966,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys)); }, print_variant_index)) { + // Per-part gradient: PrintRegionConfig alone cannot reveal a change in which slots + // have per-part enabled, so compare against the snapshot taken when these regions + // were generated and regenerate on any difference (slot toggled, per-part moved + // between slots, eligibility changed via components / gradient / is_mixed). + if (print_object_regions->last_slot_per_part_enabled != slot_per_part_enabled) { + invalidate(); + model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; + print_regions_reshuffled = true; + } // Regions are valid, just keep them. } else { // Regions were reshuffled. @@ -1884,7 +1997,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value), painting_extruders, print_variant_index, - print_object.is_fuzzy_skin_painted()); + print_object.is_fuzzy_skin_painted(), + slot_per_part_enabled); } for (auto it = it_print_object; it != it_print_object_end; ++it) if ((*it)->m_shared_regions) { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 8083da954e..2a4eb8d7a7 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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("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("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 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 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; } diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 255c8721b9..330151c4d3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -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 diff --git a/src/libslic3r/TexturePainting.cpp b/src/libslic3r/TexturePainting.cpp new file mode 100644 index 0000000000..9f83f282cd --- /dev/null +++ b/src/libslic3r/TexturePainting.cpp @@ -0,0 +1,726 @@ +#include "TexturePainting.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#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 buf(img.data.begin(), img.data.end()); + cv::Mat raw(1, static_cast(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 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>& 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(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(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>& 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> 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>& out_uv_coords) +{ + std::vector 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(mat_idx) >= textured.material_texture_map.size()) + return -1; + const int ti = textured.material_texture_map[mat_idx]; + if (ti < 0 || static_cast(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 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(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 mat_solid_y; // mat_idx -> y row in atlas + std::map> 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(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(mat_solid_y[kv.first], 0) = cv::Vec3b( + static_cast(std::clamp(c[2] * 255.f, 0.f, 255.f)), + static_cast(std::clamp(c[1] * 255.f, 0.f, 255.f)), + static_cast(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(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(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(atlas_w); + const float v_atlas = (y_off + 0.5f) / static_cast(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(atlas_w); + const float v_atlas = (y_off + v * th) / static_cast(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> 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> 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> 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& rgb1, + const std::array& rgba2) +{ + return tex2color::color_utils::calc_rgb_color_difference_by_ciede2000( + rgb1, + { + static_cast(rgba2[0] * 255.0f), + static_cast(rgba2[1] * 255.0f), + static_cast(rgba2[2] * 255.0f) + }); +} + +std::vector match_clusters_to_filaments( + const std::vector>& cluster_colors, + const std::vector>& filament_colors, + const std::vector& /*filament_names*/) +{ + std::vector matches(cluster_colors.size()); + + for (size_t ci = 0; ci < cluster_colors.size(); ++ci) { + matches[ci].cluster_index = static_cast(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(fi); + matches[ci].filament_color = filament_colors[fi]; + } + } + } + return matches; +} + +bool apply_painted_mesh_to_volume( + const PaintedMesh& painted, + const std::vector& matches, + ModelVolume& volume) +{ + if (painted.face_colors.empty() || matches.empty()) + return false; + + const auto& cluster_colors = painted.cluster_colors; + std::map, 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()); + 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( + static_cast(EnforcerBlockerType::Extruder1) + extruder_idx); + if (state <= EnforcerBlockerType::ExtruderMax) + selector.set_facet(static_cast(fi), state); + } + } + + volume.mmu_segmentation_facets.set(selector); + return true; +} + +bool decode_texture_to_pixels( + const TextureImage& img, + std::vector& 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 sample_face_from_uvs( + const cv::Mat& tex, + const std::array& uv0, + const std::array& uv1, + const std::array& 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(fx), 0, tex.cols - 1); + int y0 = std::clamp(static_cast(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 { + const uchar* ptr = tex.data + row * tex.step[0] + col * ch; + return {static_cast(ptr[2]), static_cast(ptr[1]), static_cast(ptr[0])}; + }; + + auto c00 = sample(y0, x0); + auto c10 = sample(y0, x1); + auto c01 = sample(y1, x0); + auto c11 = sample(y1, x1); + + std::array 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::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 sample_face_from_texture( + const cv::Mat& tex, + const std::vector>& uvs, + const std::array& face) +{ + std::array uv0 = {0.f, 0.f}, uv1 = {0.f, 0.f}, uv2 = {0.f, 0.f}; + if (face[0] >= 0 && static_cast(face[0]) < uvs.size()) uv0 = uvs[face[0]]; + if (face[1] >= 0 && static_cast(face[1]) < uvs.size()) uv1 = uvs[face[1]]; + if (face[2] >= 0 && static_cast(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>& out_face_colors) +{ + if (textured.indices.empty()) + return false; + + // Decode all textures up front + std::vector 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(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(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 { + int idx = ui[vi]; + if (idx >= 0 && static_cast(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(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::clamp(c[0] * 255.f, 0.f, 255.f)), + static_cast(std::clamp(c[1] * 255.f, 0.f, 255.f)), + static_cast(std::clamp(c[2] * 255.f, 0.f, 255.f)) + }; + } else { + out_face_colors[fi] = {192, 192, 192}; // default gray + } + } + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/TexturePainting.hpp b/src/libslic3r/TexturePainting.hpp new file mode 100644 index 0000000000..fc4688620b --- /dev/null +++ b/src/libslic3r/TexturePainting.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +struct indexed_triangle_set; + +namespace Slic3r { + +class TriangleMesh; +class ModelVolume; + +struct TextureImage { + int width = 0; + int height = 0; + int channels = 4; + std::vector data; +}; + +struct TexturedMesh { + std::vector> vertices; + std::vector> indices; + std::vector> uvs; + std::vector textures; + std::vector material_ids; + // material index -> index in textures[] (-1 if no texture, use material_colors) + std::vector material_texture_map; + // per-material baseColorFactor (RGBA 0-1), indexed by material index + std::vector> material_colors; + + // Per-face independent UV support (for OBJ where the same vertex can have + // different texture coordinates on different faces). + std::vector> uv_coords; // UV coordinate pool + std::vector> 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> 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> precomputed_vertex_colors; +}; + +struct PaintedMesh { + std::vector> vertices; + std::vector> indices; + std::vector> face_colors; // per-face RGB [0..255] + std::vector> cluster_colors; +}; + +using PaintProgressCallback = std::function; +using PaintCancelCallback = std::function; +using PaintMeshRepairCallback = std::function progress_callback, + std::function 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 cluster_color = {0,0,0}; + std::array 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 match_clusters_to_filaments( + const std::vector>& cluster_colors, + const std::vector>& filament_colors, + const std::vector& filament_names); + +double compute_delta_e( + const std::array& rgb1, + const std::array& rgba2); + +bool apply_painted_mesh_to_volume( + const PaintedMesh& painted, + const std::vector& 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& 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>& out_face_colors); + +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/Callbacks.hpp b/src/libslic3r/TextureToColor/Callbacks.hpp new file mode 100644 index 0000000000..70084f585d --- /dev/null +++ b/src/libslic3r/TextureToColor/Callbacks.hpp @@ -0,0 +1,15 @@ +#pragma once +#include + +namespace Slic3r { namespace tex2color { + +struct AlgoProgress { + int percent = 0; + const char* message = ""; +}; + +using AlgoProgressCallback = std::function; +using AlgoCancelCallback = std::function; + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/CgalUtils.hpp b/src/libslic3r/TextureToColor/CgalUtils.hpp new file mode 100644 index 0000000000..109d454827 --- /dev/null +++ b/src/libslic3r/TextureToColor/CgalUtils.hpp @@ -0,0 +1,173 @@ +#pragma once +#include "TriMesh.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace tex2color { +namespace cgalutils { + +using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel; +using CGALMesh = CGAL::Surface_mesh; + +inline CGALMesh trimesh_to_cgal(const TriMesh& mesh) { + CGALMesh cm; + std::vector 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 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> vtx_to_adj_faces(mesh.vertices.size()); + std::size_t edge_id = 0; + std::vector> edge_to_faces; + std::vector> vtx_to_prev_vtxs(mesh.vertices.size()); + std::vector> vtx_to_next_vtxs(mesh.vertices.size()); + std::vector> 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(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()); + } + 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 visited_faces; + std::queue 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(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& vertex_uvs, + CGALMesh& cgal_mesh, std::vector& cgal_vertex_uvs) +{ + cgal_mesh.clear(); + std::vector 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 diff --git a/src/libslic3r/TextureToColor/ColorUtils.cpp b/src/libslic3r/TextureToColor/ColorUtils.cpp new file mode 100644 index 0000000000..7127a5d364 --- /dev/null +++ b/src/libslic3r/TextureToColor/ColorUtils.cpp @@ -0,0 +1,1643 @@ +#include "ColorUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CgalUtils.hpp" +#include "libslic3r/AABBTreeIndirect.hpp" +#include + +namespace Slic3r { namespace tex2color { +namespace color_utils { + +// #define DEBUG_FLAG + +#ifndef M_PI +#define M_PI 3.1415926535897932 +#endif + +#ifndef EPSILON +#define EPSILON 1e-6 +#endif + +#ifndef DOUBLE_LIMITS +#define DOUBLE_LIMITS +#define Double_MAX std::numeric_limits::max() +#define Double_MIN -std::numeric_limits::max() +#endif // !DOUBLE_LIMITS + +namespace PMP = CGAL::Polygon_mesh_processing; + +using cgalutils::CGALMesh; +using CGALKernel = cgalutils::Kernel; + +static constexpr double TOPO_SMOOTH_WEIGHT_THRESHOLD = 0.3; + +namespace detail { +template +double average_edge_length_impl(const Mesh& m) { + double total = 0.0; + size_t count = 0; + for (auto e : m.edges()) { + auto h = m.halfedge(e); + auto p0 = m.point(m.source(h)); + auto p1 = m.point(m.target(h)); + total += std::sqrt(CGAL::squared_distance(p0, p1)); + ++count; + } + return count > 0 ? total / count : 1.0; +} +} // namespace detail + +typedef CGAL::Aff_transformation_3 Affine_transformation_3; +typedef boost::graph_traits::halfedge_descriptor halfedge_descriptor; +typedef boost::graph_traits::edge_descriptor edge_descriptor; +typedef boost::graph_traits::vertex_descriptor vertex_descriptor; +typedef CGAL::AABB_face_graph_triangle_primitive Primitive; +typedef CGAL::AABB_traits Traits; +typedef CGAL::AABB_tree Tree; +typedef CGALMesh::template Property_map VNMap; + +static inline ColorDouble convert_rgb_uint_to_rgb_double(const Color& color) { + return ColorDouble{static_cast(color[0]), static_cast(color[1]), static_cast(color[2])}; +} + +static void normalize(CGALKernel::Vector_3& vec) { + double squared_length = vec.squared_length(); + if (squared_length > EPSILON) { + vec /= sqrt(squared_length); + } +} + +static double get_angle_between_vectors(const CGALKernel::Vector_3& v1, const CGALKernel::Vector_3& v2) { + CGALKernel::Vector_3 dir1{v1}, dir2{v2}; + normalize(dir1); + normalize(dir2); + double product_dot = dir1.x() * dir2.x() + dir1.y() * dir2.y() + dir1.z() * dir2.z(); + if (product_dot > 1.0 - EPSILON) { + return 0.0; + } else if (product_dot < -1.0 + EPSILON) { + return 180.0; + } + return std::acos(product_dot) / M_PI * 180.0; +} + +static void calc_face_normals(const CGALMesh& mesh, std::vector& face_normals) { + std::size_t fcnt = mesh.number_of_faces(); + face_normals.resize(fcnt); + for (auto face : mesh.faces()) { + std::vector points; + for (auto vtx : mesh.vertices_around_face(mesh.halfedge(face))) { + points.push_back(mesh.point(vtx)); + } + Eigen::Vector3d pos1(points[0].x(), points[0].y(), points[0].z()); + Eigen::Vector3d pos2(points[1].x(), points[1].y(), points[1].z()); + Eigen::Vector3d pos3(points[2].x(), points[2].y(), points[2].z()); + face_normals[face] = (pos3 - pos2).cross(pos1 - pos2); + face_normals[face].normalize(); + } + return; +} + +static bool check_and_repair_self_intersect(CGALMesh& mesh, bool* is_self_intersect_status = nullptr) { + // true means the mesh is no self intersect now + // false means the mesh is still self intersect + auto is_self_intersect = PMP::does_self_intersect(mesh); + if (is_self_intersect_status != nullptr) { + *is_self_intersect_status = is_self_intersect; + } + if (is_self_intersect) { + bool repair = PMP::experimental::remove_self_intersections(mesh); + if (repair) { + return true; + } else { + return false; + } + } + return true; +} + +static bool save_polylines(const std::string& file_name, const std::vector>& polylines) { + std::vector points; + std::vector> lines; + for (auto& polyline : polylines) { + std::size_t begin_pt_idx = points.size(); + for (auto& pt : polyline) { + points.push_back(pt); + } + for (std::size_t i = 1; i < polyline.size(); ++i) { + lines.emplace_back(begin_pt_idx + i, begin_pt_idx + i + 1); // obj is begin at 1 + } + } + std::ofstream output_file(file_name, std::ios::out); + for (auto& point : points) { + output_file << "v " << point[0] << " " << point[1] << " " << point[2] << "\n"; + } + for (auto& line : lines) { + output_file << "l " << line.first << " " << line.second << "\n"; + } + output_file.close(); + return true; +} + +static bool smooth_region_topo_boundary(CGALMesh& mesh, std::vector& face_labels, std::size_t max_iters = 20) { + // Topological smoothing: reassign face labels + std::size_t iter = 0; + while (iter < max_iters) { + ++iter; + bool flip_flag = false; + for (auto face : mesh.faces()) { + std::size_t same_label_count = 0; + std::unordered_map map_label_to_cnt; + std::size_t max_adj_cnt = 0; + std::size_t max_adj_label = face_labels[face]; + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(face))) { + if (adj_face == CGALMesh::null_face() || !mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { + continue; + } + if (face_labels[adj_face] == face_labels[face]) { + ++same_label_count; + } else { + ++map_label_to_cnt[face_labels[adj_face]]; + if (map_label_to_cnt[face_labels[adj_face]] > max_adj_cnt) { + max_adj_cnt = map_label_to_cnt[face_labels[adj_face]]; + max_adj_label = face_labels[adj_face]; + } + } + } + if (max_adj_cnt > same_label_count) { + face_labels[face] = max_adj_label; + flip_flag = true; + } + } + + if (!flip_flag) { + break; + } + } + return true; +} + +static bool smooth_region_geom_boundary(CGALMesh& mesh, std::vector& face_labels, const SmoothParameters& smooth_parameters) { + // 1. extract boundary vertices and make polines + std::unordered_map map_vtx_to_degree; + std::unordered_set segment_boundary_edges; + std::unordered_set feature_edges; + std::unordered_set feature_vertices; + constexpr double feature_angle = 45; + for (const auto& edge : mesh.edges()) { + if (!mesh.is_valid(edge) || mesh.is_border(edge)) { + continue; + } + auto source = mesh.source(mesh.halfedge(edge)); + auto target = mesh.target(mesh.halfedge(edge)); + auto face_1 = mesh.face(mesh.halfedge(edge)); + auto face_2 = mesh.face(mesh.opposite(mesh.halfedge(edge))); + auto normal_1 = PMP::compute_face_normal(face_1, mesh); + auto normal_2 = PMP::compute_face_normal(face_2, mesh); + double angle = get_angle_between_vectors(normal_1, normal_2); + if (angle > feature_angle) { + feature_edges.insert(edge); + feature_vertices.insert(source); + feature_vertices.insert(target); + } + + if (face_labels[face_1] == face_labels[face_2]) { + continue; + } + + segment_boundary_edges.insert(edge); + ++map_vtx_to_degree[source]; + ++map_vtx_to_degree[target]; + } + + // 2. smooth each polyline + std::vector> polylines; + std::unordered_set visited_edges; + + std::function&)> trace_polyline = [&](std::vector& polyline) -> void { + if (polyline.empty()) { + return; + } + CGAL::SM_Vertex_index curr_vtx = polyline.back(); + if (map_vtx_to_degree[curr_vtx] != 2) { + return; + } + for (const auto& halfedge : mesh.halfedges_around_target(mesh.halfedge(curr_vtx))) { + CGAL::SM_Edge_index edge = mesh.edge(halfedge); + if (visited_edges.count(edge) || !segment_boundary_edges.count(edge)) { + continue; + } + visited_edges.insert(edge); + CGAL::SM_Vertex_index adj_vtx = mesh.source(halfedge); + if (!map_vtx_to_degree.count(adj_vtx)) { + continue; + } + polyline.push_back(adj_vtx); + return trace_polyline(polyline); + } + }; + + // 2.1. open polyline: from T nodes search other 2-degree nodes + for (auto& [src_vtx, degree] : map_vtx_to_degree) { + if (degree == 2) { + continue; + } + for (auto& src_halfedge : mesh.halfedges_around_target(mesh.halfedge(src_vtx))) { + CGAL::SM_Edge_index src_edge = mesh.edge(src_halfedge); + if (visited_edges.count(src_edge) || !segment_boundary_edges.count(src_edge)) { + continue; + } + visited_edges.insert(src_edge); + CGAL::SM_Vertex_index adj_vtx = mesh.source(src_halfedge); + if (!map_vtx_to_degree.count(adj_vtx)) { + continue; + } + std::vector polyline{src_vtx, adj_vtx}; + trace_polyline(polyline); + polylines.push_back(std::move(polyline)); + } + } + + // 2.2. closed polylines + for (auto edge : segment_boundary_edges) { + if (visited_edges.count(edge)) { + continue; + } + visited_edges.insert(edge); + CGAL::SM_Halfedge_index halfedge = mesh.halfedge(edge); + std::vector polyline{mesh.source(halfedge), mesh.target(halfedge)}; + trace_polyline(polyline); + if (polyline.front() != polyline.back()) { + std::cerr << "[Error]: loop polyline but not closed!!!\n"; + } + polylines.push_back(std::move(polyline)); + } + + // 3. smooth boundary + Tree boundary_tree(mesh.faces().begin(), mesh.faces().end(), mesh); + boundary_tree.accelerate_distance_queries(); + + constexpr std::size_t max_iters = 5; + const double smooth_weight = smooth_parameters.smooth_weight; // Controls smoothing intensity; larger values produce smoother results. Range: 0.1~1.0. + double origin_weight = std::max(1.0 - smooth_weight, 0.0); + for (std::size_t iter = 0; iter < max_iters; ++iter) { + for (const auto& polyline : polylines) { + std::size_t pt_cnt = polyline.size(); + std::vector points(pt_cnt); + for (std::size_t pt_idx = 1; pt_idx + 1 < pt_cnt; ++pt_idx) { + std::vector pts{mesh.point(polyline[pt_idx - 1]), mesh.point(polyline[pt_idx + 1])}; + CGALKernel::Point_3 smooth_pt = CGAL::ORIGIN + ((CGAL::centroid(pts.begin(), pts.end()) - CGAL::ORIGIN) * smooth_weight + + (mesh.point(polyline[pt_idx]) - CGAL::ORIGIN) * origin_weight); + points[pt_idx] = boundary_tree.closest_point(smooth_pt); + } + + if (polyline.front() == polyline.back()) { + if (feature_vertices.count(polyline.front())) { + continue; + } + std::vector pts{mesh.point(polyline[1]), mesh.point(polyline[pt_cnt - 2])}; + CGALKernel::Point_3 smooth_pt = CGAL::ORIGIN + ((CGAL::centroid(pts.begin(), pts.end()) - CGAL::ORIGIN) * smooth_weight + + (mesh.point(polyline.front()) - CGAL::ORIGIN) * origin_weight); + mesh.point(polyline.front()) = boundary_tree.closest_point(smooth_pt); + } + + for (std::size_t pt_idx = 1; pt_idx + 1 < pt_cnt; ++pt_idx) { + if (feature_vertices.count(polyline[pt_idx])) { + continue; + } + mesh.point(polyline[pt_idx]) = points[pt_idx]; + } + } + } + + for (auto& polyline : polylines) { + for (auto& vtx : polyline) { + mesh.point(vtx) = boundary_tree.closest_point(mesh.point(vtx)); + } + } + + return true; +} + +// HSV, XYZ, and LAB are only used internally for computing color differences, so they are declared in this cpp file only. +typedef std::array HSV; +typedef std::array XYZ; // Intermediate space for converting between LAB and RGB +typedef std::array LAB; // CIELAB was designed to match human visual perception; the standard method for perceptual color difference +// Common white points +const XYZ D65_WHITE = {0.95047, 1.0, 1.08883}; + +/** + * @brief Convert an RGB color to the HSV color space. + * + * @param rgb Input RGB color [R, G, B], range 0~255. + * @return HSV output [H, S, V], H in 0~360, S and V in 0~1. + */ +static HSV convert_rgb_to_hsv(const RGB& rgb) { + // Normalize to [0, 1] + double r = rgb[0] / 255.0; + double g = rgb[1] / 255.0; + double b = rgb[2] / 255.0; + + double max = std::max({r, g, b}); + double min = std::min({r, g, b}); + double delta = max - min; + + // Compute hue H + double h = 0; + if (delta == 0) { + h = 0; // Gray; hue is undefined + } else { + if (max == r) { + h = 60.0 * fmod((g - b) / delta, 6.0); + } else if (max == g) { + h = 60.0 * ((b - r) / delta + 2.0); + } else { // max == b + h = 60.0 * ((r - g) / delta + 4.0); + } + if (h < 0) { + h += 360.0; + } + } + + // Compute saturation S + double s = (max == 0) ? 0 : (delta / max); + + // Compute value V + double v = max; + + return {h, s, v}; +} + +/** + * @brief Convert an HSV color to the RGB color space. + * + * @param hsv Input HSV color [H, S, V], H in 0~360, S and V in 0~1. + * @return RGB output [R, G, B], range 0~255. + */ +static RGB convert_hsv_to_rgb(const HSV& hsv) { + double h = hsv[0]; + double s = hsv[1]; + double v = hsv[2]; + + double c = v * s; + double x = c * (1 - std::abs(fmod(h / 60.0, 2.0) - 1)); + double m = v - c; + + double r, g, b; + + if (h < 60) { + r = c; + g = x; + b = 0; + } else if (h < 120) { + r = x; + g = c; + b = 0; + } else if (h < 180) { + r = 0; + g = c; + b = x; + } else if (h < 240) { + r = 0; + g = x; + b = c; + } else if (h < 300) { + r = x; + g = 0; + b = c; + } else { + r = c; + g = 0; + b = x; + } + + return {static_cast((r + m) * 255 + 0.5), static_cast((g + m) * 255 + 0.5), static_cast((b + m) * 255 + 0.5)}; +} + +static XYZ convert_rgb_to_xyz(const RGB& color_rgb) { + ColorDouble rgb{static_cast(color_rgb[0]), static_cast(color_rgb[1]), static_cast(color_rgb[2])}; + auto gammaCorrect = [](double v) -> double { + v = v / 255.0; + if (v > 0.04045) { + return std::pow((v + 0.055) / 1.055, 2.4); + } else { + return v / 12.92; + } + }; + + double r = gammaCorrect(rgb[0]); + double g = gammaCorrect(rgb[1]); + double b = gammaCorrect(rgb[2]); + + // sRGB to XYZ matrix + return {r * 0.4124564 + g * 0.3575761 + b * 0.1804375, r * 0.2126729 + g * 0.7151522 + b * 0.0721750, r * 0.0193339 + g * 0.1191920 + b * 0.9503041}; +} + +// sRGB non-linear channel values [0,1] (double) -> linear light -> XYZ; equivalent to convert_rgb_to_xyz when v=n/255 +static XYZ convert_srgb01_to_xyz(double rs, double gs, double bs) { + auto gamma_correct = [](double v) -> double { + v = std::clamp(v, 0.0, 1.0); + return (v > 0.04045) ? std::pow((v + 0.055) / 1.055, 2.4) : (v / 12.92); + }; + const double r = gamma_correct(rs); + const double g = gamma_correct(gs); + const double b = gamma_correct(bs); + return {r * 0.4124564 + g * 0.3575761 + b * 0.1804375, r * 0.2126729 + g * 0.7151522 + b * 0.0721750, r * 0.0193339 + g * 0.1191920 + b * 0.9503041}; +} + +static LAB convert_xyz_to_lab(const XYZ& xyz) { + auto f = [](double t) -> double { + const double delta = 6.0 / 29.0; + if (t > delta * delta * delta) { + return std::cbrt(t); + } else { + return t / (3.0 * delta * delta) + 4.0 / 29.0; + } + }; + + // D65 white point + double xn = D65_WHITE[0], yn = D65_WHITE[1], zn = D65_WHITE[2]; + + double fx = f(xyz[0] / xn); + double fy = f(xyz[1] / yn); + double fz = f(xyz[2] / zn); + + return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)}; +} + +// ColorDouble here represents sRGB [0,1]; see calc_rgb_color_difference_by_ciede2000_srgb01 header comment +static LAB convert_srgb01_to_lab(const ColorDouble& srgb01) { + return convert_xyz_to_lab(convert_srgb01_to_xyz(srgb01[0], srgb01[1], srgb01[2])); +} + +static LAB convert_rgb_to_lab(const RGB& rgb) { + return convert_xyz_to_lab(convert_rgb_to_xyz(rgb)); +} + +static Color convert_lab_to_rgb(const LAB& lab) { + // Lab → XYZ + const double delta = 6.0 / 29.0; + const double delta2x3 = 3.0 * delta * delta; + + double fy = (lab[0] + 16.0) / 116.0; + double fx = lab[1] / 500.0 + fy; + double fz = fy - lab[2] / 200.0; + + double x = D65_WHITE[0] * (fx > delta ? fx * fx * fx : delta2x3 * (fx - 4.0 / 29.0)); + double y = D65_WHITE[1] * (fy > delta ? fy * fy * fy : delta2x3 * (fy - 4.0 / 29.0)); + double z = D65_WHITE[2] * (fz > delta ? fz * fz * fz : delta2x3 * (fz - 4.0 / 29.0)); + + // XYZ -> linear RGB (sRGB inverse matrix) + double r_lin = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z; + double g_lin = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z; + double b_lin = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z; + + // linear RGB -> sRGB (inverse gamma correction) + auto inverse_gamma = [](double v) -> double { + v = std::max(v, 0.0); + return v <= 0.0031308 ? 12.92 * v : 1.055 * std::pow(v, 1.0 / 2.4) - 0.055; + }; + + auto to_uint8 = [](double v) -> std::size_t { return static_cast(std::clamp(std::round(v * 255.0), 0.0, 255.0)); }; + + return {to_uint8(inverse_gamma(r_lin)), to_uint8(inverse_gamma(g_lin)), to_uint8(inverse_gamma(b_lin))}; +} + +/** + * @brief CIEDE2000 color-difference computation. + * + * Currently the most accurate color-difference formula, recommended by CIE as the industry standard. + * + * @param lab1 LAB values of the first color. + * @param lab2 LAB values of the second color. + * @return Color difference (typically < 1.0 is imperceptible to the human eye). + */ +static double ciede2000(const std::array& lab1, const std::array& lab2) { + // Parameters in the CIE L*C*h* formula + double L1 = lab1[0], a1 = lab1[1], b1 = lab1[2]; + double L2 = lab2[0], a2 = lab2[1], b2 = lab2[2]; + + // Compute C1 and C2 + double C1 = std::sqrt(a1 * a1 + b1 * b1); + double C2 = std::sqrt(a2 * a2 + b2 * b2); + double C_avg = (C1 + C2) / 2.0; + + // G factor (compensates for non-linearity in the mid-low chroma region) + double C7 = C_avg * C_avg * C_avg * C_avg * C_avg * C_avg * C_avg; + double G = 0.5 * (1.0 - std::sqrt(C7 / (C7 + 6103515625.0))); + + // a1' and a2' + double a1_prime = (1.0 + G) * a1; + double a2_prime = (1.0 + G) * a2; + + // C'1 and C'2 + double C1_prime = std::sqrt(a1_prime * a1_prime + b1 * b1); + double C2_prime = std::sqrt(a2_prime * a2_prime + b2 * b2); + double C_prime_avg = (C1_prime + C2_prime) / 2.0; + + // h'1 and h'2 + double h1_prime = std::atan2(b1, a1_prime); + double h2_prime = std::atan2(b2, a2_prime); + if (h1_prime < 0) { + h1_prime += 2 * M_PI; + } + if (h2_prime < 0) { + h2_prime += 2 * M_PI; + } + + // Compute dh' + double dh_prime; + if (std::abs(h1_prime - h2_prime) <= M_PI) { + dh_prime = h2_prime - h1_prime; + } else if (h2_prime <= h1_prime) { + dh_prime = h2_prime - h1_prime + 2 * M_PI; + } else { + dh_prime = h2_prime - h1_prime - 2 * M_PI; + } + + // Compute dH' + double dH_prime = 2.0 * std::sqrt(C1_prime * C2_prime) * std::sin(dh_prime / 2.0); + + // Compute dL' + double dL_prime = L2 - L1; + + // Compute dC' + double dC_prime = C2_prime - C1_prime; + + // Compute h_prime_avg + double h_prime_avg; + if (std::abs(h1_prime - h2_prime) > M_PI) { + h_prime_avg = (h1_prime + h2_prime + 2 * M_PI) / 2.0; + } else { + h_prime_avg = (h1_prime + h2_prime) / 2.0; + } + + // Compute T + double T = 1.0 - 0.17 * std::cos(h_prime_avg - M_PI / 6.0) + 0.24 * std::cos(2.0 * h_prime_avg) + 0.32 * std::cos(3.0 * h_prime_avg + M_PI / 30.0) - + 0.20 * std::cos(4.0 * h_prime_avg - 3.0 * M_PI / 6.0); + + // Compute rotation term R_T = -R_C * sin(2*delta_theta), where delta_theta = 30 * exp(-((h_bar'-275)/25)^2) + // h_prime_avg is in radians; convert to degrees for delta_theta; 2*delta_theta = 60 * exp(...), convert back to radians for sin + double h_prime_avg_deg = h_prime_avg * 180.0 / M_PI; + double C_prime_avg_7 = std::pow(C_prime_avg, 7); + double R = -2.0 * std::sqrt(C_prime_avg_7 / (C_prime_avg_7 + 6103515625.0)) * + std::sin((60.0 * M_PI / 180.0) * std::exp(-std::pow((h_prime_avg_deg - 275.0) / 25.0, 2))); + + // Compute SL, SC, SH + double L_prime_avg = (L1 + L2) / 2.0; + double SL = 1.0 + 0.015 * std::pow(L_prime_avg - 50.0, 2) / std::sqrt(20 + std::pow(L_prime_avg - 50.0, 2)); + double SC = 1.0 + 0.045 * C_prime_avg; + double SH = 1.0 + 0.015 * C_prime_avg * T; + + // Final color difference + double deltaE = std::sqrt(std::pow(dL_prime / SL, 2) + std::pow(dC_prime / SC, 2) + std::pow(dH_prime / SH, 2) + R * (dC_prime / SC) * (dH_prime / SH)); + + return deltaE; +} + +double calc_rgb_color_difference_by_ciede2000(const RGB& rgb1, const RGB& rgb2) { + auto lab1 = convert_rgb_to_lab(rgb1); + auto lab2 = convert_rgb_to_lab(rgb2); + return ciede2000(lab1, lab2); +} + +double calc_rgb_color_difference_by_ciede2000_srgb01(const ColorDouble& rgb1, const ColorDouble& rgb2) { + const LAB lab1 = convert_srgb01_to_lab(rgb1); + const LAB lab2 = convert_srgb01_to_lab(rgb2); + return ciede2000(lab1, lab2); +} + +// Working-space distance function type: inputs are two colors in the same space (RGB-double or Lab) +using WorkingDistFunc = double (*)(const ColorDouble&, const ColorDouble&); + +// Farthest Point Sampling (FPS) initialization algorithm. +// The first center is the point nearest to the global centroid; subsequent centers are the points farthest from the existing set. +static std::vector farthest_point_sampling_init(const std::vector& working_colors, std::size_t k, WorkingDistFunc dist_func) { + std::vector centers(k); + + // 1. Compute the global centroid and pick the nearest point as the first center + ColorDouble centroid = {0.0, 0.0, 0.0}; + for (const auto& c : working_colors) { + centroid[0] += c[0]; + centroid[1] += c[1]; + centroid[2] += c[2]; + } + const auto n = static_cast(working_colors.size()); + centroid[0] /= n; + centroid[1] /= n; + centroid[2] /= n; + + double best_dist = std::numeric_limits::max(); + std::size_t first_idx = 0; + for (std::size_t i = 0; i < working_colors.size(); ++i) { + double d = dist_func(working_colors[i], centroid); + if (d < best_dist) { + best_dist = d; + first_idx = i; + } + } + centers[0] = working_colors[first_idx]; + + // Minimum distance from each point to the already-selected center set + std::vector min_distances(working_colors.size(), std::numeric_limits::max()); + + // 2. Greedily select the remaining K-1 centers: pick the point with the largest min_distance each time + for (std::size_t i = 1; i < k; ++i) { + const ColorDouble& last_center = centers[i - 1]; + + // Update each point's minimum distance with the newly added center + double farthest_dist = -1.0; + std::size_t farthest_idx = 0; + for (std::size_t c_idx = 0; c_idx < working_colors.size(); ++c_idx) { + double d = dist_func(working_colors[c_idx], last_center); + if (d < min_distances[c_idx]) { + min_distances[c_idx] = d; + } + if (min_distances[c_idx] > farthest_dist) { + farthest_dist = min_distances[c_idx]; + farthest_idx = c_idx; + } + } + + centers[i] = working_colors[farthest_idx]; + } + + return centers; +} + +bool remesh_mesh(TriMesh& bbs_mesh, std::vector& face_labels, double target_edge_length_ratio) { + if (face_labels.size() != bbs_mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "Input mesh face count does not match label count"; + return false; + } + + // Back up face labels for recovery after remeshing. + std::vector face_labels_of_original_mesh(face_labels); + + CGALMesh cgal_mesh; + cgalutils::convert_trimesh_to_cgal(bbs_mesh, cgal_mesh); + + // AABBTreeIndirect references vertices/faces externally, so snapshot the + // pre-remesh geometry by moving it out of bbs_mesh (which is overwritten + // below with the post-remesh mesh). std::move on std::vector is O(1). + TriVertices old_vertices = std::move(bbs_mesh.vertices); + TriFaces old_indices = std::move(bbs_mesh.indices); + auto original_mesh_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices); + + std::unordered_set feature_edges; + std::unordered_set feature_vertices; + CGALMesh::Property_map constrained_edges = + cgal_mesh.add_property_map("constrained_edges", false).first; + CGALMesh::Property_map constrained_vertices = + cgal_mesh.add_property_map("constrained_vertices", false).first; + + // An edge is considered a geometric feature edge if its dihedral angle is less than 135 degrees (loose threshold) + constexpr double feature_angle = 135; + + auto is_feature_edge = [&](CGAL::SM_Edge_index edge) -> bool { + if (cgal_mesh.is_border(edge)) { + return true; + } + auto halfedge_1 = cgal_mesh.halfedge(edge); + auto halfedge_2 = cgal_mesh.opposite(halfedge_1); + auto face_1 = cgal_mesh.face(halfedge_1); + auto face_2 = cgal_mesh.face(halfedge_2); + if (face_labels[face_1] != face_labels[face_2]) { + // Boundary between different color regions; treated as a feature edge + return true; + } + // TODO: CGAL remeshing tends to crash when too many constrained edges are added; needs handling + //auto normal_1 = PMP::compute_face_normal(face_1, cgal_mesh); + //auto normal_2 = PMP::compute_face_normal(face_2, cgal_mesh); + //double angle = 180 - get_angle_between_vectors(normal_1, normal_2); + //BOOST_LOG_TRIVIAL(debug) << "end.\n"; + //return angle > feature_angle; + return false; + }; + + for (auto edge : cgal_mesh.edges()) { + if (is_feature_edge(edge)) { + feature_edges.insert(edge); + feature_vertices.insert(cgal_mesh.source(cgal_mesh.halfedge(edge))); + feature_vertices.insert(cgal_mesh.target(cgal_mesh.halfedge(edge))); + constrained_edges[edge] = true; + constrained_vertices[cgal_mesh.source(cgal_mesh.halfedge(edge))] = true; + constrained_vertices[cgal_mesh.target(cgal_mesh.halfedge(edge))] = true; + } + } + +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "remesh_mesh: feature_edges.size() = " << feature_edges.size() << ".\n"; + + std::vector> polylines; + for (auto edge : feature_edges) { + auto src_vtx = cgal_mesh.source(cgal_mesh.halfedge(edge)); + auto trg_vtx = cgal_mesh.target(cgal_mesh.halfedge(edge)); + polylines.push_back({cgal_mesh.point(src_vtx), cgal_mesh.point(trg_vtx)}); + } + save_polylines("ColorUtils_remesh_feature_lines.obj", polylines); +#endif // DEBUG_FLAG + + std::size_t iters = 5; +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "PMP::isotropic_remeshing start...\n"; +#endif // DEBUG_FLAG + // TODO: CGAL remeshing preserves geometric boundaries but does not maintain face labels well; colors need to be recomputed + PMP::isotropic_remeshing(cgal_mesh.faces(), target_edge_length_ratio * detail::average_edge_length_impl(cgal_mesh), cgal_mesh, + CGAL::parameters::number_of_iterations(iters) + .protect_constraints(true) + .edge_is_constrained_map(constrained_edges) + .vertex_is_constrained_map(constrained_vertices) + .collapse_constraints(true)); +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "PMP::isotropic_remeshing finshed...\n"; +#endif // DEBUG_FLAG + if (PMP::does_self_intersect(cgal_mesh)) { + PMP::experimental::remove_self_intersections(cgal_mesh); + } + + bbs_mesh.clear(); + + std::unordered_map map_cgal_vtx_to_bbs_vtx; + TriVertices bbs_vertices; + TriFaces bbs_faces; + face_labels.clear(); + face_labels.reserve(cgal_mesh.number_of_faces()); + + for (const auto& cgal_vtx : cgal_mesh.vertices()) { + if (!cgal_mesh.is_valid(cgal_vtx) || cgal_mesh.is_removed(cgal_vtx) || cgal_mesh.is_isolated(cgal_vtx)) { + continue; + } + if (!map_cgal_vtx_to_bbs_vtx.count(cgal_vtx)) { + map_cgal_vtx_to_bbs_vtx[cgal_vtx] = bbs_vertices.size(); + const auto& cgal_point = cgal_mesh.point(cgal_vtx); + bbs_vertices.emplace_back(TriVertex(cgal_point.x(), cgal_point.y(), cgal_point.z())); + } + } + + for (const auto& cgal_face : cgal_mesh.faces()) { + if (!cgal_mesh.is_valid(cgal_face) || cgal_mesh.is_removed(cgal_face)) { + continue; + } + TriFace bbs_face; + std::size_t face_vid = 0; + for (auto cgal_vtx : cgal_mesh.vertices_around_face(cgal_mesh.halfedge(cgal_face))) { + do { if (!(map_cgal_vtx_to_bbs_vtx.count(cgal_vtx))) { BOOST_LOG_TRIVIAL(warning) << "CGAL mesh contains a face with an invalid vertex"; return false; } } while(0); + bbs_face[face_vid] = map_cgal_vtx_to_bbs_vtx[cgal_vtx]; + ++face_vid; + } + bbs_faces.push_back(std::move(bbs_face)); + } + + bbs_mesh = TriMesh(bbs_faces, bbs_vertices); + + face_labels.resize(bbs_mesh.indices.size()); + tbb::parallel_for(tbb::blocked_range(0, bbs_mesh.indices.size()), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + auto& face = bbs_mesh.indices[fid]; + Vec3f face_centroid = (bbs_mesh.vertices[face[0]] + bbs_mesh.vertices[face[1]] + bbs_mesh.vertices[face[2]]) / 3.0; + size_t hit_idx = 0; + Vec3f closest; + AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + old_vertices, old_indices, original_mesh_tree, face_centroid, hit_idx, closest); + face_labels[fid] = face_labels_of_original_mesh[hit_idx]; + } + }); + + return true; +} + +bool is_closed(const TriMesh& bbs_mesh) { + CGALMesh cgal_mesh; + cgalutils::convert_trimesh_to_cgal(bbs_mesh, cgal_mesh); + +#ifdef DEBUG_FLAG + std::size_t border_edges_count = 0; + std::size_t edges_count = 0; + for (auto edge : cgal_mesh.edges()) { + if (cgal_mesh.is_border(edge)) { + ++border_edges_count; + } + ++edges_count; + } + + BOOST_LOG_TRIVIAL(debug) << "border edges count = " << border_edges_count << "\n"; + BOOST_LOG_TRIVIAL(debug) << "edges count = " << edges_count << "\n"; + + std::size_t border_faces_count = 0; + std::size_t faces_count = 0; + for (auto face : cgal_mesh.faces()) { + for (auto halfedge : cgal_mesh.halfedges_around_face(cgal_mesh.halfedge(face))) { + auto edge = cgal_mesh.edge(halfedge); + if (cgal_mesh.is_border(edge)) { + ++border_faces_count; + break; + } + } + ++faces_count; + } + + BOOST_LOG_TRIVIAL(debug) << "border faces count = " << border_faces_count << "\n"; + BOOST_LOG_TRIVIAL(debug) << "faces count = " << faces_count << "\n"; + + BOOST_LOG_TRIVIAL(debug) << "vertices count = " << cgal_mesh.number_of_vertices() << "\n"; + std::size_t num_of_components = 0; + std::unordered_set visited_faces; + for (auto src_face : cgal_mesh.faces()) { + if (visited_faces.count(src_face)) { + continue; + } + ++num_of_components; + std::queue que; + que.push(src_face); + visited_faces.insert(src_face); + while (!que.empty()) { + auto curr_face = que.front(); + que.pop(); + for (auto adj_face : cgal_mesh.faces_around_face(cgal_mesh.halfedge(curr_face))) { + if (!cgal_mesh.is_valid(adj_face) || cgal_mesh.is_removed(adj_face) || visited_faces.count(adj_face)) { + continue; + } + que.push(adj_face); + visited_faces.insert(adj_face); + } + } + } + BOOST_LOG_TRIVIAL(debug) << "components count = " << num_of_components << "\n"; +#endif // DEBUG_FLAG + + return CGAL::is_closed(cgal_mesh); +} + +static bool smooth_region_labels(TriMesh& tri_mesh, std::vector& face_labels, const SmoothParameters& smooth_parameters) { + CGALMesh mesh; + cgalutils::convert_trimesh_to_cgal(tri_mesh, mesh); + + if (mesh.number_of_faces() != face_labels.size()) { + BOOST_LOG_TRIVIAL(warning) << "Face count does not match label count"; + return false; + } + + if (smooth_parameters.smooth_weight >= TOPO_SMOOTH_WEIGHT_THRESHOLD) { + // Topological smoothing: reassign face labels. + smooth_region_topo_boundary(mesh, face_labels); + } + + if (smooth_parameters.smooth_weight > EPSILON) { + // Geometric smoothing: smooth polylines and project back onto the original mesh. + smooth_region_geom_boundary(mesh, face_labels, smooth_parameters); + } + + tri_mesh = cgalutils::cgal_to_trimesh(mesh); + + return true; +} + +bool smooth_region(TriMesh& tri_mesh, std::vector>& face_colors, const SmoothParameters& smooth_parameters) { + // Convert colors to labels + std::size_t label_next = 0; + std::vector face_labels; + face_labels.reserve(face_colors.size()); + std::map, std::size_t> map_color_to_label; + std::unordered_map> map_label_to_color; + for (auto& color : face_colors) { + if (!map_color_to_label.count(color)) { + map_color_to_label[color] = label_next; + map_label_to_color[label_next] = color; + ++label_next; + } + face_labels.push_back(map_color_to_label[color]); + } + + if (!smooth_region_labels(tri_mesh, face_labels, smooth_parameters)) + return false; + + // Convert labels back to colors + for (std::size_t fid = 0; fid < face_labels.size(); ++fid) { + face_colors[fid] = map_label_to_color[face_labels[fid]]; + } + + return true; +} + +bool smooth_region(TriMesh& tri_mesh, std::vector& face_labels, const SmoothParameters& smooth_parameters) { + return smooth_region_labels(tri_mesh, face_labels, smooth_parameters); +} + +// Compute the squared Euclidean distance between two colors (RGB vectors) +double calc_rgb_color_difference_by_squared_rgb_double(const ColorDouble& c1, const ColorDouble& c2) { + double dr = c1[0] - c2[0]; + double dg = c1[1] - c2[1]; + double db = c1[2] - c2[2]; + return dr * dr + dg * dg + db * db; +} + +// Compute the squared Euclidean distance between two colors (RGB vectors) +double calc_rgb_color_difference_by_squared_rgb(const RGB& c1, const RGB& c2) { + auto c1d = convert_rgb_uint_to_rgb_double(c1); + auto c2d = convert_rgb_uint_to_rgb_double(c2); + return calc_rgb_color_difference_by_squared_rgb_double(c1d, c2d); +} + +// K-Means core: run FPS initialization + assign/update iterations in working space, return cluster centers +static std::vector kmeans_core(const std::vector& working_colors, std::size_t k, std::size_t max_iter, WorkingDistFunc dist_func, + const std::function& cancel_cb = nullptr) { + std::vector centers = farthest_point_sampling_init(working_colors, k, dist_func); + std::vector assignments(working_colors.size()); + + for (std::size_t iter = 0; iter < max_iter; ++iter) { + if (cancel_cb && cancel_cb()) return centers; + std::atomic changed(false); + + tbb::parallel_for(tbb::blocked_range(0, working_colors.size()), [&](const tbb::blocked_range& range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) { + double min_dist = std::numeric_limits::max(); + std::size_t best_cluster = 0; + + for (std::size_t j = 0; j < k; ++j) { + double d = dist_func(working_colors[i], centers[j]); + if (d < min_dist) { + min_dist = d; + best_cluster = j; + } + } + + if (assignments[i] != best_cluster) { + changed.store(true, std::memory_order_relaxed); + assignments[i] = best_cluster; + } + } + }); + + if (!changed.load()) { + break; + } + + std::vector new_centers(k, {0.0, 0.0, 0.0}); + std::vector counts(k, 0); + + for (std::size_t i = 0; i < working_colors.size(); ++i) { + std::size_t cluster_id = assignments[i]; + ++counts[cluster_id]; + new_centers[cluster_id][0] += working_colors[i][0]; + new_centers[cluster_id][1] += working_colors[i][1]; + new_centers[cluster_id][2] += working_colors[i][2]; + } + + for (std::size_t i = 0; i < k; ++i) { + if (counts[i] == 0) { + double max_min_dist = -1.0; + std::size_t best_idx = 0; + for (std::size_t p = 0; p < working_colors.size(); ++p) { + double nearest = std::numeric_limits::max(); + for (std::size_t c = 0; c < k; ++c) { + if (c == i || counts[c] == 0) { + continue; + } + double d = dist_func(working_colors[p], centers[c]); + if (d < nearest) { + nearest = d; + } + } + if (nearest > max_min_dist) { + max_min_dist = nearest; + best_idx = p; + } + } + centers[i] = working_colors[best_idx]; + } else { + centers[i][0] = new_centers[i][0] / counts[i]; + centers[i][1] = new_centers[i][1] / counts[i]; + centers[i][2] = new_centers[i][2] / counts[i]; + } + } + } + + return centers; +} + +// K-Means clustering algorithm +std::vector cluster_k_means(const std::vector& colors, const ClusterParameters& cluster_parameters) { + std::size_t k = cluster_parameters.cluster_k; + std::size_t max_iter = cluster_parameters.max_iter; + + if (k == 0 || colors.empty()) { + return {}; + } + + const bool use_lab = (cluster_parameters.color_difference_method != ColorDifferenceMethod::RGB); + WorkingDistFunc working_dist_func = use_lab ? ciede2000 : calc_rgb_color_difference_by_squared_rgb_double; + + // ========================================== + // 1. Preprocessing: deduplicate + pre-convert to working space + // ========================================== + std::vector unique_colors = colors; + std::sort(unique_colors.begin(), unique_colors.end()); + auto last = std::unique(unique_colors.begin(), unique_colors.end()); + unique_colors.erase(last, unique_colors.end()); + +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "Input colors: " << colors.size() << ", Unique colors: " << unique_colors.size(); +#endif // DEBUG_FLAG + + if (unique_colors.size() < k) { + BOOST_LOG_TRIVIAL(warning) << "Unique color count (" << unique_colors.size() << ") is less than target K (" << k << "). Adjusting K."; + k = unique_colors.size(); + if (k == 0) { + return {}; + } + return unique_colors; + } + + std::vector working_colors(unique_colors.size()); + for (std::size_t i = 0; i < unique_colors.size(); ++i) { + if (use_lab) { + working_colors[i] = convert_rgb_to_lab(unique_colors[i]); + } else { + working_colors[i] = {static_cast(unique_colors[i][0]), static_cast(unique_colors[i][1]), static_cast(unique_colors[i][2])}; + } + } + + // ========================================== + // 2. K-Means clustering + // ========================================== + auto centers = kmeans_core(working_colors, k, max_iter, working_dist_func, cluster_parameters.cancel_callback); + + // ========================================== + // 3. Output: convert from working space back to RGB + // ========================================== + std::vector result(k); + for (std::size_t i = 0; i < k; ++i) { + if (use_lab) { + result[i] = convert_lab_to_rgb(centers[i]); + } else { + result[i] = {static_cast(std::round(centers[i][0])), static_cast(std::round(centers[i][1])), + static_cast(std::round(centers[i][2]))}; + } + } + + return result; +} + +std::vector cluster_to_specified_colors(const std::vector& colors, const std::vector& specified_colors) { + std::vector cluster_colors = colors; + std::vector specified_double_colors; + specified_double_colors.reserve(specified_colors.size()); + for (auto& color : specified_colors) { + specified_double_colors.push_back(ColorDouble{static_cast(color[0]), static_cast(color[1]), static_cast(color[2])}); + } + + tbb::parallel_for(tbb::blocked_range(0, cluster_colors.size()), [&](const tbb::blocked_range& range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) { + ColorDouble p_color{static_cast(cluster_colors[i][0]), static_cast(cluster_colors[i][1]), + static_cast(cluster_colors[i][2])}; + + double min_dist = std::numeric_limits::max(); + std::size_t best_cluster = 0; + + for (std::size_t j = 0; j < specified_double_colors.size(); ++j) { + double d = calc_rgb_color_difference_by_squared_rgb_double(p_color, specified_double_colors[j]); + if (d < min_dist) { + min_dist = d; + best_cluster = j; + } + } + + cluster_colors[i] = specified_colors[best_cluster]; + } + }); + + return cluster_colors; +} + +// Color PCA struct +struct ColorPCA { + std::size_t color_idx; // Index of the original color in ColorList + double pca_value; // Projection value onto the first principal component + + ColorPCA(std::size_t c_idx, double pca_val) + : color_idx(c_idx), + pca_value(pca_val) {} + + // Comparison operator (ascending order) + bool operator<(const ColorPCA& other) const { return pca_value < other.pca_value; } + + // Equality check + bool operator==(const ColorPCA& other) const { return color_idx == other.color_idx; } +}; + +[[maybe_unused]] static std::vector sort_colors_by_pca(const std::vector& colors) { + const std::size_t n = colors.size(); + + if (n == 0) { + return {}; + } + + if (n == 1) { + return {{0, 0.0}}; + } + + // Step 1: Data preprocessing - normalize to [0, 1] + Eigen::MatrixXd data(n, 3); + + for (std::size_t i = 0; i < n; ++i) { + data(i, 0) = static_cast(colors[i][0]) / 255.0; // R + data(i, 1) = static_cast(colors[i][1]) / 255.0; // G + data(i, 2) = static_cast(colors[i][2]) / 255.0; // B + } + + // Step 2: Compute mean and center the data + Eigen::RowVector3d mean = data.colwise().mean(); + Eigen::MatrixXd centered = data.rowwise() - mean; + + // Step 3: Compute covariance matrix (3x3) + Eigen::Matrix3d cov = (centered.adjoint() * centered) / static_cast(n - 1); + + // Step 4: Eigenvalue decomposition + Eigen::SelfAdjointEigenSolver solver(cov); + + if (solver.info() != Eigen::Success) { +#ifdef DEBUG_FLAG + std::cerr << "PCA: Eigenvalue decomposition failed" << std::endl; +#endif // DEBUG_FLAG + // Fallback: return an approximate result sorted by luminance. + // Luminance is a key perceptual feature; convert RGB to grayscale (L = 0.299R + 0.587G + 0.114B) and sort in ascending order. + std::vector result; + result.reserve(n); + for (std::size_t i = 0; i < n; ++i) { + double luminance = 0.299 * colors[i][0] + 0.587 * colors[i][1] + 0.114 * colors[i][2]; + result.push_back({i, luminance}); + } + std::sort(result.begin(), result.end()); + return result; + } + + // Get eigenvalues and eigenvectors (sorted by eigenvalue in descending order) + Eigen::Vector3d eigenvalues = solver.eigenvalues(); + Eigen::Matrix3d eigenvectors = solver.eigenvectors(); + + // Step 5: Find the eigenvector corresponding to the largest eigenvalue (first principal component) + Eigen::MatrixXd::Index max_eigenvalue_idx; + eigenvalues.maxCoeff(&max_eigenvalue_idx); + + Eigen::Vector3d first_principal_component = eigenvectors.col(max_eigenvalue_idx); + + // Step 6: Project centered data onto the first principal component + Eigen::VectorXd projections = centered * first_principal_component; + + // Step 7: Build result and sort + std::vector result; + result.reserve(n); + + for (std::size_t i = 0; i < n; ++i) { + result.push_back({i, projections(i)}); + } + + std::sort(result.begin(), result.end()); + + return result; +} + +std::vector cluster_adaptive(const std::vector& colors, const ClusterParameters& cluster_parameters) { + if (colors.empty()) { + return {}; + } + + const double max_color_distance = cluster_parameters.max_color_distance; + const std::size_t max_iter = cluster_parameters.max_iter; + const bool use_lab = (cluster_parameters.color_difference_method != ColorDifferenceMethod::RGB); + WorkingDistFunc working_dist_func = use_lab ? ciede2000 : calc_rgb_color_difference_by_squared_rgb_double; + + // ========================================== + // 1. Deduplicate + convert to working space + // ========================================== + std::vector unique_colors = colors; + std::sort(unique_colors.begin(), unique_colors.end()); + auto last = std::unique(unique_colors.begin(), unique_colors.end()); + unique_colors.erase(last, unique_colors.end()); + + BOOST_LOG_TRIVIAL(debug) << "cluster_adaptive: colors=" << colors.size() + << " unique=" << unique_colors.size() + << " max_color_distance=" << max_color_distance; + + if (unique_colors.size() <= 1) { + return unique_colors; + } + + std::vector working_colors(unique_colors.size()); + for (std::size_t i = 0; i < unique_colors.size(); ++i) { + if (use_lab) { + working_colors[i] = convert_rgb_to_lab(unique_colors[i]); + } else { + working_colors[i] = {static_cast(unique_colors[i][0]), static_cast(unique_colors[i][1]), static_cast(unique_colors[i][2])}; + } + } + + // ========================================== + // 2. Binary search k: find the smallest k where P99 radius <= max_color_distance + // ========================================== + const std::size_t max_k = cluster_parameters.max_cluster_k; + std::size_t lo = 1; + std::size_t hi = std::min(max_k, unique_colors.size()); + std::size_t best_k = 0; + std::vector best_centers; + + constexpr double kRadiusPercentile = 0.99; + + auto calc_max_radius = [&](const std::vector& centers) -> double { + std::vector distances(working_colors.size()); + tbb::parallel_for(tbb::blocked_range(0, working_colors.size()), [&](const tbb::blocked_range& range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) { + double min_dist = std::numeric_limits::max(); + for (const auto& center : centers) { + double d = working_dist_func(working_colors[i], center); + if (d < min_dist) { + min_dist = d; + } + } + distances[i] = min_dist; + } + }); + if (distances.empty()) { + return 0.0; + } + std::size_t idx = std::min(static_cast(distances.size() * kRadiusPercentile), distances.size() - 1); + std::nth_element(distances.begin(), distances.begin() + idx, distances.end()); + return distances[idx]; + }; + + const auto& cancel_cb = cluster_parameters.cancel_callback; + + while (lo <= hi) { + if (cancel_cb && cancel_cb()) return {}; + std::size_t mid = lo + (hi - lo) / 2; + auto centers = kmeans_core(working_colors, mid, max_iter, working_dist_func, cancel_cb); + if (cancel_cb && cancel_cb()) return {}; + double max_radius = calc_max_radius(centers); + + BOOST_LOG_TRIVIAL(debug) << "cluster_adaptive binary search: k=" << mid << " max_radius=" << max_radius; + + if (max_radius <= max_color_distance) { + best_k = mid; + best_centers = std::move(centers); + hi = mid - 1; + } else { + lo = mid + 1; + } + } + + if (best_k == 0) { + best_k = std::min(max_k, unique_colors.size()); + best_centers = kmeans_core(working_colors, best_k, max_iter, working_dist_func, cancel_cb); + BOOST_LOG_TRIVIAL(warning) << "cluster_adaptive: binary search found no k satisfying max_radius<=" + << max_color_distance << ", fallback to k=" << best_k; + } + + BOOST_LOG_TRIVIAL(debug) << "cluster_adaptive: best_k=" << best_k; + + // ========================================== + // 3. Convert centers back to RGB + // ========================================== + std::vector result(best_k); + for (std::size_t i = 0; i < best_k; ++i) { + if (use_lab) { + result[i] = convert_lab_to_rgb(best_centers[i]); + } else { + result[i] = {static_cast(std::round(best_centers[i][0])), static_cast(std::round(best_centers[i][1])), + static_cast(std::round(best_centers[i][2]))}; + } + } + + return result; +} + +static std::vector> get_connected_face_groups(const CGALMesh& mesh) { + std::vector> face_groups; + std::unordered_set visited_faces; + for (auto src_face : mesh.faces()) { + if (visited_faces.count(src_face)) { + continue; + } + std::vector face_group; + std::queue que; + que.push(src_face); + visited_faces.insert(src_face); + while (!que.empty()) { + auto curr_face = que.front(); + que.pop(); + face_group.push_back(curr_face); + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(curr_face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face) || visited_faces.count(adj_face)) { + continue; + } + que.push(adj_face); + visited_faces.insert(adj_face); + } + } + face_groups.push_back(std::move(face_group)); + } + return face_groups; +} + +bool get_components(const TriMesh& bbs_mesh, const std::vector& bbs_vertex_uvs, std::vector& component_meshes, + std::vector>& component_vertex_uvs) { + component_meshes.clear(); + component_vertex_uvs.clear(); + + if (bbs_mesh.vertices.size() != bbs_vertex_uvs.size()) { + BOOST_LOG_TRIVIAL(warning) << "Input mesh vertex count does not match texture coordinate count"; + return false; + } + + CGALMesh cgal_mesh; + std::vector cgal_vertex_uvs; + if (!cgalutils::convert_trimesh_to_cgal(bbs_mesh, bbs_vertex_uvs, cgal_mesh, cgal_vertex_uvs)) { + BOOST_LOG_TRIVIAL(warning) << "Mesh conversion failed"; + return false; + } + + auto face_groups = get_connected_face_groups(cgal_mesh); + + for (const auto& faces : face_groups) { + std::unordered_map map_cgal_vtx_to_bbs_vtx; + TriVertices bbs_vertices; + TriFaces bbs_faces; + std::vector bbs_vertex_uvs; + + for (auto cgal_face : faces) { + TriFace bbs_face; + std::size_t face_vid = 0; + for (auto cgal_vtx : cgal_mesh.vertices_around_face(cgal_mesh.halfedge(cgal_face))) { + if (!map_cgal_vtx_to_bbs_vtx.count(cgal_vtx)) { + map_cgal_vtx_to_bbs_vtx[cgal_vtx] = bbs_vertices.size(); + const auto& cgal_point = cgal_mesh.point(cgal_vtx); + bbs_vertices.push_back(TriVertex(cgal_point.x(), cgal_point.y(), cgal_point.z())); + bbs_vertex_uvs.push_back(cgal_vertex_uvs[cgal_vtx]); + } + bbs_face[face_vid] = map_cgal_vtx_to_bbs_vtx[cgal_vtx]; + ++face_vid; + } + bbs_faces.push_back(std::move(bbs_face)); + } + + component_meshes.push_back(TriMesh(bbs_faces, bbs_vertices)); + component_vertex_uvs.push_back(std::move(bbs_vertex_uvs)); + } + + return true; +} + +bool calc_nearest_color_id(const std::vector& colors, const RGB& color, std::size_t& nearest_color_id) { + if (colors.empty()) { + BOOST_LOG_TRIVIAL(warning) << "No color list provided"; + return false; + } + double min_dist = std::numeric_limits::max(); + nearest_color_id = 0; + for (std::size_t i = 0; i < colors.size(); ++i) { + double dist = calc_rgb_color_difference_by_ciede2000(colors[i], color); + if (dist < min_dist) { + min_dist = dist; + nearest_color_id = i; + } + } + return true; +} + +bool mesh_cluster(const TriMesh& bbs_mesh, const std::vector& cluster_centers, std::vector& map_face_to_rgb, + std::vector& map_face_to_cluster_id) { + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "No cluster centers provided"; + return false; + } + + CGALMesh mesh; + cgalutils::convert_trimesh_to_cgal(bbs_mesh, mesh); + if (mesh.number_of_faces() != bbs_mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "CGAL mesh face count does not match BBS mesh face count"; + return false; + } + if (mesh.number_of_faces() != map_face_to_rgb.size()) { + BOOST_LOG_TRIVIAL(warning) << "CGAL mesh face count does not match RGB count"; + return false; + } + + if (cluster_centers.size() == 1) { + std::fill(map_face_to_rgb.begin(), map_face_to_rgb.end(), cluster_centers[0]); +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "input cluster centers' size is 1, so we set all face RGB as same with it and return.\n"; +#endif + return true; + } + + std::vector map_face_to_area(mesh.number_of_faces()); + tbb::parallel_for(tbb::blocked_range(0, mesh.number_of_faces()), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + CGAL::SM_Face_index face(fid); + map_face_to_area[fid] = std::max(PMP::face_area(face, mesh), EPSILON); + } + }); + + // Step 1: Identify faces that definitely belong to a cluster center. + // A face is definitively assigned when dist1 * absolute_difference_times < dist2 (nearest vs. second-nearest center). + constexpr double absolute_difference_times = 1.5; + // 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 + constexpr double difference_epsilon = 3.0; + constexpr std::size_t invalid_cluster_id = std::numeric_limits::max(); + map_face_to_cluster_id.resize(mesh.number_of_faces(), invalid_cluster_id); + std::vector>> map_face_to_dists(mesh.number_of_faces(), + std::vector>(cluster_centers.size())); + tbb::parallel_for(tbb::blocked_range(0, mesh.number_of_faces()), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + CGAL::SM_Face_index face(fid); + std::vector>& dist_and_cid_vec = map_face_to_dists[fid]; + for (std::size_t cluster_id = 0; cluster_id < cluster_centers.size(); ++cluster_id) { + double dist = calc_rgb_color_difference_by_ciede2000(map_face_to_rgb[fid], cluster_centers[cluster_id]); + //dist_and_cid_vec.emplace_back(dist, cluster_id); + dist_and_cid_vec[cluster_id] = std::pair{dist, cluster_id}; + } + std::sort(dist_and_cid_vec.begin(), dist_and_cid_vec.end()); + if (dist_and_cid_vec[0].first < difference_epsilon || dist_and_cid_vec[0].first * absolute_difference_times < dist_and_cid_vec[1].first) { + map_face_to_cluster_id[fid] = dist_and_cid_vec[0].second; + } + } + }); + + std::unordered_set unclusted_fids; + for (std::size_t fid = 0; fid < mesh.number_of_faces(); ++fid) { + if (map_face_to_cluster_id[fid] == invalid_cluster_id) { + unclusted_fids.insert(fid); + } + } + + auto convert_unclustered_to_clustered = [&](const std::unordered_set& iter_clustered_fids) -> bool { + if (iter_clustered_fids.empty()) { + return false; + } + for (auto& fid : iter_clustered_fids) { + unclusted_fids.erase(fid); + } + return true; + }; + + // Step 2: Flood. Use faces computed in the previous step as seeds and propagate outward. + while (!unclusted_fids.empty()) { + bool changed = false; + std::unordered_set iter_clustered_fids; + // If an uncolored face has an adjacent color whose count exceeds the sum of all other colors, assign that color + for (auto fid : unclusted_fids) { + CGAL::SM_Face_index face(fid); + std::size_t count = 0; + std::unordered_map map_cluster_id_to_count; + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { + continue; + } + ++count; + ++map_cluster_id_to_count[map_face_to_cluster_id[adj_face]]; + } + for (auto& [cluster_id, cnt] : map_cluster_id_to_count) { + if (cluster_id != invalid_cluster_id && cnt * 2 > count) { + map_face_to_cluster_id[fid] = cluster_id; + iter_clustered_fids.insert(fid); + break; + } + } + } + changed = convert_unclustered_to_clustered(iter_clustered_fids) || changed; + iter_clustered_fids.clear(); + + // If a face's nearest cluster center (by color distance) happens to have an adjacent face, assign that color too + for (auto fid : unclusted_fids) { + CGAL::SM_Face_index face(fid); + std::unordered_set adj_cluster_ids; + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { + continue; + } + if (map_face_to_cluster_id[adj_face] == invalid_cluster_id) { + continue; + } + adj_cluster_ids.insert(map_face_to_cluster_id[adj_face]); + } + if (adj_cluster_ids.count(map_face_to_dists[fid].front().second)) { + map_face_to_cluster_id[fid] = map_face_to_dists[fid].front().second; + iter_clustered_fids.insert(fid); + } + } + changed = convert_unclustered_to_clustered(iter_clustered_fids) || changed; + iter_clustered_fids.clear(); + + // If no face colors were modified in this iteration, stop + if (!changed) { + break; + } + } + + // Step 3: Handle remaining unclustered faces (run after the above operations complete) + constexpr bool use_average_color = true; + for (auto src_fid : std::vector(unclusted_fids.begin(), unclusted_fids.end())) { + if (!unclusted_fids.count(src_fid)) { + continue; + } + // Compute connected unclustered faces + std::queue que; + std::unordered_set connected_unclustered_faces; + que.push(src_fid); + connected_unclustered_faces.insert(src_fid); + double sum_r = 0, sum_g = 0, sum_b = 0; + double sum_area = 0.0; + std::unordered_map map_cluster_id_to_adj_area; + while (!que.empty()) { + auto curr_fid = que.front(); + que.pop(); + sum_r += map_face_to_area[curr_fid] * map_face_to_rgb[curr_fid][0]; + sum_g += map_face_to_area[curr_fid] * map_face_to_rgb[curr_fid][1]; + sum_b += map_face_to_area[curr_fid] * map_face_to_rgb[curr_fid][2]; + sum_area += map_face_to_area[curr_fid]; + CGAL::SM_Face_index curr_face(curr_fid); + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(curr_face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { // Invalid face + continue; + } + if (map_face_to_cluster_id[adj_face] != invalid_cluster_id) { + map_cluster_id_to_adj_area[map_face_to_cluster_id[adj_face]] += map_face_to_area[adj_face]; + } else { + if (!connected_unclustered_faces.count(adj_face)) { // Already clustered or already recorded + que.push(adj_face); + connected_unclustered_faces.insert(adj_face); + } + } + } + } + std::size_t matched_cluster_id = invalid_cluster_id; + if (use_average_color) { + // Use average color + RGB average_color{static_cast(sum_r / sum_area), static_cast(sum_g / sum_area), + static_cast(sum_b / sum_area)}; + double min_dist = std::numeric_limits::max(); + for (auto& [cluster_id, area] : map_cluster_id_to_adj_area) { + double dist = calc_rgb_color_difference_by_ciede2000(average_color, cluster_centers[cluster_id]); + if (dist < min_dist) { + min_dist = dist; + matched_cluster_id = cluster_id; + } + } + } else { + // Use adjacent area + double adj_max_area = 0.0; + for (auto& [cluster_id, area] : map_cluster_id_to_adj_area) { + if (area > adj_max_area) { + adj_max_area = area; + matched_cluster_id = cluster_id; + } + } + } + for (auto fid : connected_unclustered_faces) { + map_face_to_cluster_id[fid] = matched_cluster_id; + unclusted_fids.erase(fid); + } + } + + // Convert cluster center IDs to colors + for (std::size_t fid = 0; fid < mesh.number_of_faces(); ++fid) { + if (map_face_to_cluster_id[fid] == invalid_cluster_id) { + map_face_to_rgb[fid] = cluster_centers[0]; + map_face_to_cluster_id[fid] = 0; // Prevent out-of-bounds errors when using cluster_id later + } else { + map_face_to_rgb[fid] = cluster_centers[map_face_to_cluster_id[fid]]; + } + } + + return true; +} + +} // namespace color_utils + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/ColorUtils.hpp b/src/libslic3r/TextureToColor/ColorUtils.hpp new file mode 100644 index 0000000000..05849109f4 --- /dev/null +++ b/src/libslic3r/TextureToColor/ColorUtils.hpp @@ -0,0 +1,207 @@ +#pragma once + +#include "Callbacks.hpp" +#include "TriMesh.hpp" + +namespace Slic3r { namespace tex2color { + +namespace color_utils { +struct ClusterParameters; + +typedef std::array Color; // RGB: [R, G, B] 0~255 +typedef std::vector ColorList; +typedef std::array ColorDouble; +typedef std::array 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 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 cluster_k_means(const std::vector& 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 cluster_adaptive(const std::vector& 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 cluster_to_specified_colors(const std::vector& colors, const std::vector& 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& 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>& 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& 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& vertex_uvs, std::vector& component_meshes, + std::vector>& 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& 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& cluster_centers, std::vector& map_face_to_rgb, + std::vector& map_face_to_cluster_id); + +} // namespace color_utils + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/Repair.hpp b/src/libslic3r/TextureToColor/Repair.hpp new file mode 100644 index 0000000000..44dc30c144 --- /dev/null +++ b/src/libslic3r/TextureToColor/Repair.hpp @@ -0,0 +1,252 @@ +#pragma once +#include "TriMesh.hpp" +#include "CgalUtils.hpp" +#include "Callbacks.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::halfedge_descriptor; + + std::vector 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::halfedge_descriptor; + using FaceDescriptor = boost::graph_traits::face_descriptor; + + PMP::stitch_borders(cgal_mesh); + PMP::duplicate_non_manifold_vertices(cgal_mesh); + + std::vector border_cycles; + PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles)); + + for (const HalfedgeDescriptor h : border_cycles) { + std::vector 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& 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(Clock::now() - t0).count(); + }; + + const Clock::time_point t_total = Clock::now(); + + // Convert TriMesh to polygon soup (point container + triangle index container) + std::vector soup_points; + std::vector> 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(f[0]), + static_cast(f[1]), + static_cast(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 out; + { + const auto t0 = Clock::now(); + out = std::make_shared(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 diff --git a/src/libslic3r/TextureToColor/TextureToColor.cpp b/src/libslic3r/TextureToColor/TextureToColor.cpp new file mode 100644 index 0000000000..e5dde36714 --- /dev/null +++ b/src/libslic3r/TextureToColor/TextureToColor.cpp @@ -0,0 +1,1043 @@ +#include "TextureToColor.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "CgalUtils.hpp" +#include "ColorUtils.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include +#include +#include "Repair.hpp" +#include "libslic3r/AABBTreeIndirect.hpp" +#include + +namespace Slic3r { namespace tex2color { + +using namespace color_utils; + +// #define OUTPUT_TEST_RESULT + +static void SaveToOFF(const std::string& path, const TriMesh& mesh, const std::vector& face_colors) +{ + std::filesystem::create_directories(std::filesystem::path(path).parent_path()); + std::ofstream ofs(path); + if (!ofs.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "SaveToOFF: failed to open " << path; + return; + } + + const auto& vertices = mesh.vertices; + const auto& faces = mesh.indices; + + ofs << "OFF\n"; + ofs << vertices.size() << " " << faces.size() << " 0\n"; + + for (const auto& v : vertices) { + ofs << v.x() << " " << v.y() << " " << v.z() << "\n"; + } + + for (std::size_t i = 0; i < faces.size(); ++i) { + const auto& f = faces[i]; + ofs << "3 " << f[0] << " " << f[1] << " " << f[2]; + if (i < face_colors.size()) { + ofs << " " << face_colors[i][0] / 255.0 + << " " << face_colors[i][1] / 255.0 + << " " << face_colors[i][2] / 255.0 + << " 1.0"; + } + ofs << "\n"; + } +} + +static std::vector count_cluster_label_usage(const std::vector& face_labels, std::size_t cluster_count) +{ + std::vector usage(cluster_count, 0); + for (std::size_t label : face_labels) { + if (label < cluster_count) { + ++usage[label]; + } + } + return usage; +} + +static bool discard_unused_cluster_centers(std::vector& cluster_centers, std::vector& face_labels, const char* stage_name) +{ + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot discard unused cluster centers at " << stage_name + << ", no cluster center is available."; + return false; + } + + const std::vector usage = count_cluster_label_usage(face_labels, cluster_centers.size()); + std::vector label_remap(cluster_centers.size(), std::numeric_limits::max()); + std::vector used_cluster_centers; + used_cluster_centers.reserve(cluster_centers.size()); + + for (std::size_t cluster_id = 0; cluster_id < cluster_centers.size(); ++cluster_id) { + if (usage[cluster_id] == 0) { + continue; + } + label_remap[cluster_id] = used_cluster_centers.size(); + used_cluster_centers.push_back(cluster_centers[cluster_id]); + } + + if (used_cluster_centers.size() == cluster_centers.size()) { + return true; + } + if (used_cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot discard unused cluster centers at " << stage_name + << ", no face uses any valid cluster center."; + return false; + } + + for (std::size_t& label : face_labels) { + if (label >= label_remap.size() || label_remap[label] == std::numeric_limits::max()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot remap cluster label " << label + << " at " << stage_name << "."; + return false; + } + label = label_remap[label]; + } + + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: discarded " << (cluster_centers.size() - used_cluster_centers.size()) + << " unused adaptive cluster centers at " << stage_name << "."; + cluster_centers = std::move(used_cluster_centers); + return true; +} + +static bool ensure_all_cluster_centers_used(const std::vector& source_face_colors, const std::vector& cluster_centers, + std::vector& face_labels, const char* stage_name) +{ + if (source_face_colors.size() != face_labels.size()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot preserve cluster colors at " << stage_name + << ", face color count does not match label count."; + return false; + } + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot preserve cluster colors at " << stage_name + << ", no cluster center is available."; + return false; + } + if (cluster_centers.size() > face_labels.size()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot use all cluster centers at " << stage_name + << ", centers=" << cluster_centers.size() << " faces=" << face_labels.size() << "."; + return false; + } + + std::vector usage = count_cluster_label_usage(face_labels, cluster_centers.size()); + std::size_t missing_count = 0; + for (std::size_t cluster_id = 0; cluster_id < usage.size(); ++cluster_id) { + if (usage[cluster_id] != 0) { + continue; + } + ++missing_count; + + double best_cost = std::numeric_limits::max(); + std::size_t best_face_id = std::numeric_limits::max(); + std::size_t best_old_cluster_id = std::numeric_limits::max(); + + for (std::size_t fid = 0; fid < face_labels.size(); ++fid) { + const std::size_t old_cluster_id = face_labels[fid]; + if (old_cluster_id >= cluster_centers.size() || usage[old_cluster_id] <= 1) { + continue; + } + + const double old_dist = calc_rgb_color_difference_by_ciede2000(source_face_colors[fid], cluster_centers[old_cluster_id]); + const double new_dist = calc_rgb_color_difference_by_ciede2000(source_face_colors[fid], cluster_centers[cluster_id]); + const double cost = new_dist - old_dist; + if (cost < best_cost) { + best_cost = cost; + best_face_id = fid; + best_old_cluster_id = old_cluster_id; + } + } + + if (best_face_id == std::numeric_limits::max()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: failed to assign a seed face for unused cluster " << cluster_id + << " at " << stage_name << "."; + continue; + } + + face_labels[best_face_id] = cluster_id; + --usage[best_old_cluster_id]; + ++usage[cluster_id]; + } + + if (missing_count > 0) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: reassigned seed faces for " << missing_count + << " unused cluster centers at " << stage_name << "."; + } + + for (std::size_t count : usage) { + if (count == 0) { + return false; + } + } + return true; +} + +// Bilinear interpolation texture sampling; sub-pixel precision avoids nearest-neighbor aliasing +static RGB get_pixel_color(float u, float v, const cv::Mat& texture) { + u = u - std::floor(u); + v = v - std::floor(v); + + // glTF UV convention: (0,0) = top-left, v increases downward + float fx = u * (texture.cols - 1); + float fy = v * (texture.rows - 1); + + int x0 = std::clamp(static_cast(fx), 0, texture.cols - 1); + int y0 = std::clamp(static_cast(fy), 0, texture.rows - 1); + int x1 = std::min(x0 + 1, texture.cols - 1); + int y1 = std::min(y0 + 1, texture.rows - 1); + + float wx = fx - x0; + float wy = fy - y0; + + const int ch = texture.channels(); + auto sample = [&](int row, int col) -> std::array { + const uchar* ptr = texture.data + row * texture.step[0] + col * ch; + return {static_cast(ptr[2]), static_cast(ptr[1]), static_cast(ptr[0])}; + }; + + auto c00 = sample(y0, x0); + auto c10 = sample(y0, x1); + auto c01 = sample(y1, x0); + auto c11 = sample(y1, x1); + + // Bilinear blend: lerp(lerp(c00,c10,wx), lerp(c01,c11,wx), wy) + RGB color; + for (int i = 0; i < 3; ++i) { + float top = c00[i] * (1.0f - wx) + c10[i] * wx; + float bot = c01[i] * (1.0f - wx) + c11[i] * wx; + color[i] = static_cast(std::clamp(top * (1.0f - wy) + bot * wy, 0.0f, 255.0f)); + } + return color; +} + +// 7-point triangular Gaussian quadrature barycentric coordinates and weights (precision sufficient for capturing texture detail within faces) +static constexpr std::array, 7> GAUSS_TRI_BARY = {{ + {1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f}, + {0.059715871f, 0.470142064f, 0.470142064f}, + {0.470142064f, 0.059715871f, 0.470142064f}, + {0.470142064f, 0.470142064f, 0.059715871f}, + {0.797426985f, 0.101286507f, 0.101286507f}, + {0.101286507f, 0.797426985f, 0.101286507f}, + {0.101286507f, 0.101286507f, 0.797426985f}, +}}; +static constexpr std::array GAUSS_TRI_WEIGHT = {0.225f, 0.132394152f, 0.132394152f, 0.132394152f, 0.125939181f, 0.125939181f, 0.125939181f}; +static_assert( + []() constexpr { + float sum = 0.0f; + for (auto w : GAUSS_TRI_WEIGHT) { + sum += w; + } + return sum > 0.999f && sum < 1.001f; + }(), + "Sum of Gaussian quadrature weights must be 1.0"); + +// Multi-point Gaussian quadrature sampling on a single face; returns weighted average color. +// GAUSS_TRI_WEIGHT sums to 1.0 (Hammer quadrature formula), no normalization needed. +static RGB sample_face_color(const std::array& uvs, const cv::Mat& texture) { + float r = 0.0f, g = 0.0f, b = 0.0f; + for (int k = 0; k < 7; ++k) { + float u = GAUSS_TRI_BARY[k][0] * uvs[0].x() + GAUSS_TRI_BARY[k][1] * uvs[1].x() + GAUSS_TRI_BARY[k][2] * uvs[2].x(); + float v = GAUSS_TRI_BARY[k][0] * uvs[0].y() + GAUSS_TRI_BARY[k][1] * uvs[1].y() + GAUSS_TRI_BARY[k][2] * uvs[2].y(); + RGB c = get_pixel_color(u, v, texture); + float w = GAUSS_TRI_WEIGHT[k]; + r += w * c[0]; + g += w * c[1]; + b += w * c[2]; + } + return RGB{static_cast(std::clamp(r, 0.0f, 255.0f)), static_cast(std::clamp(g, 0.0f, 255.0f)), + static_cast(std::clamp(b, 0.0f, 255.0f))}; +} + +// Use array instead of vector for UV storage to avoid per-face heap allocations at million-face scale +using FaceUVArray = std::array; + +static bool linear_subdivision(TriMesh& mesh, std::vector& uv_coords, const std::function& sub_progress = nullptr) { + const auto& original_vertices = mesh.vertices; + const auto& original_faces = mesh.indices; + TriVertices sub_vertices = mesh.vertices; + sub_vertices.reserve(original_vertices.size() + original_faces.size() * 3); + TriFaces sub_faces; + std::vector sub_uv_coords; + + // Single-level flat map with edge key encoding replaces nested unordered_map; + // merges two vertex indices into a single uint64_t to reduce hash lookups and indirection. + if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] { + BOOST_LOG_TRIVIAL(warning) << "[boundary] " << __FUNCTION__ << " vertex_count=" << original_vertices.size() << " exceeds 32-bit edge_key encoding range, skipping subdivision"; + return false; + } + auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t { + return a < b ? ((static_cast(a) << 32) | b) : ((static_cast(b) << 32) | a); + }; + std::unordered_map map_edge_to_sub_vtx; + map_edge_to_sub_vtx.reserve(original_faces.size() * 3 / 2); + + for (const auto& face : original_faces) { + for (std::size_t i = 0; i < 3; ++i) { + std::size_t vtx_1 = face[i]; + std::size_t vtx_2 = face[(i + 1) % 3]; + uint64_t key = edge_key(vtx_1, vtx_2); + if (map_edge_to_sub_vtx.count(key) > 0) { + continue; + } + TriVertex edge_vtx = (original_vertices[vtx_1] + original_vertices[vtx_2]) * 0.5; + map_edge_to_sub_vtx[key] = sub_vertices.size(); + sub_vertices.push_back(edge_vtx); + } + } + if (sub_progress) { + sub_progress(50); + } + + // Subdivide faces and their UVs: each original face splits into 4 sub-faces (parallel writes, no contention) + const std::size_t N = original_faces.size(); + sub_faces.resize(N * 4); + sub_uv_coords.resize(N * 4); + std::atomic has_missing_edge{false}; + + tbb::parallel_for(tbb::blocked_range(0, N), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + const std::size_t base = fid * 4; + const auto& face = original_faces[fid]; + std::size_t vtx_0 = face[0]; + std::size_t vtx_1 = face[1]; + std::size_t vtx_2 = face[2]; + + auto it01 = map_edge_to_sub_vtx.find(edge_key(vtx_0, vtx_1)); + auto it12 = map_edge_to_sub_vtx.find(edge_key(vtx_1, vtx_2)); + auto it20 = map_edge_to_sub_vtx.find(edge_key(vtx_2, vtx_0)); + if (it01 == map_edge_to_sub_vtx.end() || it12 == map_edge_to_sub_vtx.end() || it20 == map_edge_to_sub_vtx.end()) [[unlikely]] { + has_missing_edge.store(true, std::memory_order_relaxed); + Vec3i32 degen(vtx_0, vtx_0, vtx_0); + FaceUVArray degen_uv = {uv_coords[fid][0], uv_coords[fid][0], uv_coords[fid][0]}; + for (int k = 0; k < 4; ++k) { + sub_faces[base + k] = degen; + sub_uv_coords[base + k] = degen_uv; + } + continue; + } + std::size_t e01 = it01->second; + std::size_t e12 = it12->second; + std::size_t e20 = it20->second; + + const Vec2f& uv0 = uv_coords[fid][0]; + const Vec2f& uv1 = uv_coords[fid][1]; + const Vec2f& uv2 = uv_coords[fid][2]; + Vec2f uv_e01 = (uv0 + uv1) * 0.5f; + Vec2f uv_e12 = (uv1 + uv2) * 0.5f; + Vec2f uv_e20 = (uv2 + uv0) * 0.5f; + + sub_faces[base + 0] = Vec3i32(vtx_0, e01, e20); + sub_uv_coords[base + 0] = {uv0, uv_e01, uv_e20}; + + sub_faces[base + 1] = Vec3i32(e01, vtx_1, e12); + sub_uv_coords[base + 1] = {uv_e01, uv1, uv_e12}; + + sub_faces[base + 2] = Vec3i32(e01, e12, e20); + sub_uv_coords[base + 2] = {uv_e01, uv_e12, uv_e20}; + + sub_faces[base + 3] = Vec3i32(e20, e12, vtx_2); + sub_uv_coords[base + 3] = {uv_e20, uv_e12, uv2}; + } + }); + // Remove degenerate triangles (three identical vertices) to avoid impacting downstream SDF / Remesh steps + if (has_missing_edge.load(std::memory_order_relaxed)) { + std::size_t write_idx = 0; + for (std::size_t i = 0; i < sub_faces.size(); ++i) { + if (sub_faces[i][0] == sub_faces[i][1] && sub_faces[i][1] == sub_faces[i][2]) { + continue; + } + if (write_idx != i) { + sub_faces[write_idx] = sub_faces[i]; + sub_uv_coords[write_idx] = sub_uv_coords[i]; + } + ++write_idx; + } + BOOST_LOG_TRIVIAL(warning) << "[warning] linear_subdivision has missing edge vertex, removed " << (sub_faces.size() - write_idx) << " degenerate triangles"; + sub_faces.resize(write_idx); + sub_uv_coords.resize(write_idx); + } + + if (sub_progress) { + sub_progress(100); + } + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: input faces count = " << mesh.indices.size() << "."; + mesh = TriMesh(sub_faces, sub_vertices); + uv_coords = std::move(sub_uv_coords); + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: output faces count = " << mesh.indices.size() << "."; + return true; +} + +using VertexColor = std::array; + +// Quantize continuous per-vertex colors into a small palette of cluster centers. +// The legacy OBJ vertex-color import consumed discrete filament ids, so split +// decisions could be made by comparing integers. Quantizing up front restores +// that property for the adaptive splitter below. +static bool quantize_vertex_colors( + const std::vector& vertex_colors, + const TextureToColorSettings& settings, + AlgoCancelCallback cancel_callback, + std::vector& out_centers, + std::vector& out_vertex_cluster_ids) +{ + out_centers.clear(); + out_vertex_cluster_ids.clear(); + if (vertex_colors.empty()) + return false; + + std::vector vertex_rgb(vertex_colors.size()); + for (std::size_t i = 0; i < vertex_colors.size(); ++i) { + for (int c = 0; c < 3; ++c) { + float v = std::clamp(vertex_colors[i][c] * 255.0f, 0.0f, 255.0f); + vertex_rgb[i][c] = static_cast(v); + } + } + + ClusterParameters para; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; + if (settings.target_colors_num == 0) { + para.max_color_distance = settings.max_color_distance; + para.max_cluster_k = settings.max_cluster_k; + out_centers = cluster_adaptive(vertex_rgb, para); + } else { + para.cluster_k = settings.target_colors_num; + out_centers = cluster_k_means(vertex_rgb, para); + } + if (out_centers.empty()) { + BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: no cluster center generated."; + return false; + } + + out_vertex_cluster_ids.resize(vertex_rgb.size()); + for (std::size_t i = 0; i < vertex_rgb.size(); ++i) { + std::size_t nearest_id = 0; + if (!calc_nearest_color_id(out_centers, vertex_rgb[i], nearest_id)) + nearest_id = 0; + out_vertex_cluster_ids[i] = nearest_id; + } + BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: quantized " << vertex_rgb.size() + << " vertex colors into " << out_centers.size() << " clusters."; + return true; +} + +// Single-level adaptive subdivision driven by per-vertex cluster ids. +// +// Reproduces the split topology that the legacy OBJ vertex-color import encoded +// into mmu_segmentation_facets (TriangleSelector::perform_split cases 1/2/3), but +// materializes it as real geometry. An edge is split at its midpoint if and only +// if its two endpoints belong to different clusters. Because that predicate reads +// only the shared endpoints, adjacent faces always reach the same conclusion and +// no T-junctions can appear. +static bool adaptive_split_by_vertex_clusters( + TriMesh& mesh, + const std::vector& vertex_cluster_ids, + const std::vector& cluster_centers, + std::vector& out_face_colors) +{ + const TriVertices original_vertices = mesh.vertices; + const TriFaces original_faces = mesh.indices; + if (original_vertices.empty() || original_faces.empty() || cluster_centers.empty()) + return false; + if (vertex_cluster_ids.size() != original_vertices.size()) { + BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: cluster id count (" + << vertex_cluster_ids.size() << ") != vertex count (" + << original_vertices.size() << ")."; + return false; + } + if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] { + BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: vertex_count=" + << original_vertices.size() << " exceeds 32-bit edge_key range."; + return false; + } + + TriVertices out_vertices = original_vertices; + TriFaces out_faces; + out_faces.reserve(original_faces.size() * 5); + out_face_colors.clear(); + out_face_colors.reserve(original_faces.size() * 5); + + auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t { + return a < b ? ((static_cast(a) << 32) | b) + : ((static_cast(b) << 32) | a); + }; + std::unordered_map edge_to_mid; + edge_to_mid.reserve(original_faces.size() * 3 / 2); + + // Midpoints on shared edges must be deduplicated so that neighbouring faces + // reference the same vertex instead of coincident duplicates. + auto midpoint_of_edge = [&](std::size_t a, std::size_t b) -> std::size_t { + const uint64_t key = edge_key(a, b); + auto it = edge_to_mid.find(key); + if (it != edge_to_mid.end()) + return it->second; + const std::size_t idx = out_vertices.size(); + out_vertices.push_back((original_vertices[a] + original_vertices[b]) * 0.5f); + edge_to_mid.emplace(key, idx); + return idx; + }; + // Points strictly inside an original face are never shared, so they skip the map. + // The midpoint is computed before push_back so a reallocation cannot dangle it. + auto append_interior_midpoint = [&](std::size_t a, std::size_t b) -> std::size_t { + const TriVertex mid = (out_vertices[a] + out_vertices[b]) * 0.5f; + const std::size_t idx = out_vertices.size(); + out_vertices.push_back(mid); + return idx; + }; + auto emit = [&](std::size_t a, std::size_t b, std::size_t c, std::size_t cluster_id) { + out_faces.push_back(Vec3i32(static_cast(a), static_cast(b), static_cast(c))); + out_face_colors.push_back(cluster_centers[cluster_id]); + }; + + for (const auto& f : original_faces) { + const std::size_t v[3] = {static_cast(f[0]), static_cast(f[1]), static_cast(f[2])}; + const std::size_t c[3] = {vertex_cluster_ids[v[0]], vertex_cluster_ids[v[1]], vertex_cluster_ids[v[2]]}; + + // Case A: uniform cluster, keep the face untouched. + if (c[0] == c[1] && c[1] == c[2]) { + emit(v[0], v[1], v[2], c[0]); + continue; + } + + // Case B: two vertices share a cluster and the third is isolated. Split the + // two edges incident to the isolated vertex, which are exactly the + // cross-cluster ones; the opposite edge stays intact. + int iso = -1; + if (c[1] == c[2]) iso = 0; + else if (c[2] == c[0]) iso = 1; + else if (c[0] == c[1]) iso = 2; + if (iso >= 0) { + const int i = iso, j = (iso + 1) % 3, k = (iso + 2) % 3; + const std::size_t m_ij = midpoint_of_edge(v[i], v[j]); + const std::size_t m_ki = midpoint_of_edge(v[k], v[i]); + emit(v[i], m_ij, m_ki, c[i]); + emit(m_ij, v[j], m_ki, c[j]); + emit(v[j], v[k], m_ki, c[j]); + continue; + } + + // Case C: all three clusters differ. Split every edge, then cut the centre + // triangle once more. The centre is equidistant from all three clusters, so + // the legacy heuristic selects the cut by widest interior angle, which is + // the vertex opposite the longest edge. + const std::size_t m01 = midpoint_of_edge(v[0], v[1]); + const std::size_t m12 = midpoint_of_edge(v[1], v[2]); + const std::size_t m20 = midpoint_of_edge(v[2], v[0]); + emit(v[0], m01, m20, c[0]); + emit(m01, v[1], m12, c[1]); + emit(m12, v[2], m20, c[2]); + + const TriVertex& p0 = original_vertices[v[0]]; + const TriVertex& p1 = original_vertices[v[1]]; + const TriVertex& p2 = original_vertices[v[2]]; + const float sq_opposite_v0 = (p2 - p1).squaredNorm(); + const float sq_opposite_v1 = (p0 - p2).squaredNorm(); + const float sq_opposite_v2 = (p1 - p0).squaredNorm(); + int widest = 0; + float widest_len = sq_opposite_v0; + if (sq_opposite_v1 > widest_len) { widest = 1; widest_len = sq_opposite_v1; } + if (sq_opposite_v2 > widest_len) { widest = 2; } + + if (widest == 0) { + const std::size_t mc = append_interior_midpoint(m20, m01); + emit(m12, m20, mc, c[1]); + emit(mc, m01, m12, c[2]); + } else if (widest == 1) { + const std::size_t mc = append_interior_midpoint(m01, m12); + emit(m20, m01, mc, c[0]); + emit(mc, m12, m20, c[2]); + } else { + const std::size_t mc = append_interior_midpoint(m12, m20); + emit(m01, m12, mc, c[1]); + emit(mc, m20, m01, c[0]); + } + } + + BOOST_LOG_TRIVIAL(info) << "adaptive_split_by_vertex_clusters: faces " << original_faces.size() + << " -> " << out_faces.size() << ", vertices " << original_vertices.size() + << " -> " << out_vertices.size(); + mesh = TriMesh(out_faces, out_vertices); + return true; +} + +// Shared pipeline: mesh repair -> color clustering -> label assignment -> smoothing. +// Called by both TextureToColor (after UV sampling) and ClusterAndSmooth (after vertex-color oversample). +// progress_callback reports 0~100 within this function; the caller maps it to its own global range. +static bool repair_cluster_smooth( + TriMesh& mesh, + std::vector& face_colors, + std::vector& out_clustered_face_colors, + const TextureToColorSettings& settings, + AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback, + const char* log_prefix) +{ + auto report = [&](int pct, const char* msg) { + if (progress_callback) + progress_callback({pct, msg}); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << " cancelled"; + return true; + } + return false; + }; + + report(0, "Repairing mesh"); + if (cancelled()) return false; + + // Resample face colors onto a repaired mesh via centroid nearest-neighbor. + auto resample_face_colors = [&](TriMesh&& repaired_mesh) -> bool { + TriVertices old_vertices = std::move(mesh.vertices); + TriFaces old_indices = std::move(mesh.indices); + auto aabb_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices); + mesh = std::move(repaired_mesh); + + if (is_closed(mesh)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is closed."; + } else { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is open."; + } + + std::vector new_face_colors(mesh.facets_count()); + tbb::parallel_for(tbb::blocked_range(0, mesh.facets_count()), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + const auto& face = mesh.indices[fid]; + Vec3f center = (mesh.vertices[face[0]] + mesh.vertices[face[1]] + mesh.vertices[face[2]]) / 3.0f; + size_t hit_idx = 0; + Vec3f closest; + AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + old_vertices, old_indices, aabb_tree, center, hit_idx, closest); + new_face_colors[fid] = face_colors[hit_idx]; + } + }); + face_colors = std::move(new_face_colors); + return true; + }; + + auto repair_and_resample = [&]() -> bool { + std::shared_ptr repaired_mesh; + if (!RepairMesh(mesh, repaired_mesh)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": RepairMesh failed."; + return false; + } + if (cancelled()) return false; + return resample_face_colors(std::move(*repaired_mesh)); + }; + + { + TriangleMesh stats_mesh(static_cast(mesh)); + const auto& stats = stats_mesh.stats(); + // Orca's TriangleMeshStats only counts open edges: manifold() is open_edges == 0, and + // there are no separate non-manifold edge/vertex counters to test or log here. + if (!stats.manifold()) { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh has non-manifold geometry or open boundaries, open_edges=" + << stats.open_edges; + if (settings.mesh_repair_decision == MeshRepairDecision::Ask) { + if (settings.mesh_repair_decision_required) + *settings.mesh_repair_decision_required = true; + return false; + } + if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) { + indexed_triangle_set repaired_its; + std::string repair_error; + bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback( + static_cast(mesh), repaired_its, + [&](const char* message, unsigned /*percent*/) { + report(5, message ? message : "Repairing mesh"); + }, + [&]() { return cancelled(); }, &repair_error); + if (repaired) { + if (cancelled()) return false; + BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished."; + if (!resample_face_colors(TriMesh(std::move(repaired_its)))) + return false; + } else { + BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error; + } + } else { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair."; + } + } + } + + if (!cgalutils::is_mesh_halfedge_compatible(mesh)) { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh not halfedge-compatible, attempting RepairMesh."; + if (!repair_and_resample()) + return false; + } + +#ifdef OUTPUT_TEST_RESULT + SaveToOFF(std::string(log_prefix) + "_1_repair.off", mesh, face_colors); +#endif + + report(20, "Color clustering"); + if (cancelled()) return false; + + // Clustering + std::vector cluster_centers; + out_clustered_face_colors = face_colors; + std::vector clustered_face_labels(face_colors.size()); + const bool adaptive_cluster = settings.target_colors_num == 0; + + if (adaptive_cluster) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster adaptive method."; + ClusterParameters para; + para.max_color_distance = settings.max_color_distance; + para.max_cluster_k = settings.max_cluster_k; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; + cluster_centers = cluster_adaptive(face_colors, para); + if (cancelled()) return false; + } else { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster k-means method."; + ClusterParameters para; + para.cluster_k = settings.target_colors_num; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; + cluster_centers = cluster_k_means(face_colors, para); + if (cancelled()) return false; + } + + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": k = " << cluster_centers.size() << "."; + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": no cluster center generated."; + return false; + } + + report(40, "Assigning cluster labels"); + if (cancelled()) return false; + + // Assign each face to nearest cluster center + { + std::atomic done{0}; + std::atomic cancel_requested{false}; + const size_t total = mesh.indices.size(); + const size_t interval = std::max(total / 20, 1); + tbb::parallel_for(tbb::blocked_range(0, total), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + if (cancel_requested.load(std::memory_order_relaxed)) return; + std::size_t nearest_id = 0; + calc_nearest_color_id(cluster_centers, face_colors[fid], nearest_id); + clustered_face_labels[fid] = nearest_id; + out_clustered_face_colors[fid] = cluster_centers[nearest_id]; + size_t cnt = done.fetch_add(1, std::memory_order_relaxed) + 1; + if (cnt % interval == 0) { + if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; } + } + } + }); + if (cancel_requested.load() || cancelled()) return false; + } + if (adaptive_cluster) { + if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment")) + return false; + } else { + ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment"); + } + +#ifdef OUTPUT_TEST_RESULT + { + std::vector tmp = out_clustered_face_colors; + for (std::size_t i = 0; i < tmp.size(); ++i) + tmp[i] = cluster_centers[clustered_face_labels[i]]; + SaveToOFF(std::string(log_prefix) + "_3_cluster.off", mesh, tmp); + } +#endif + + report(65, "Smoothing colors"); + if (cancelled()) return false; + + SmoothParameters smooth_parameters; + smooth_parameters.smooth_weight = settings.smooth_weight; + if (!smooth_region(mesh, clustered_face_labels, smooth_parameters)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": smooth region failed."; + return false; + } + if (adaptive_cluster) { + if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing")) + return false; + } else { + ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing"); + } + + report(90, "Updating face colors"); + if (cancelled()) return false; + + for (std::size_t i = 0; i < out_clustered_face_colors.size(); ++i) + out_clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]]; + +#ifdef OUTPUT_TEST_RESULT + SaveToOFF(std::string(log_prefix) + "_4_smooth.off", mesh, out_clustered_face_colors); +#endif + + report(100, "Completed"); + return true; +} + +bool TextureToColor(const TriMesh& texture_mesh, const std::vector>& texture_mesh_uv_coords, const cv::Mat& texture, TriMesh& color_mesh, + std::vector>& face_colors, const TextureToColorSettings& settings, AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback) { + auto report = [&](int pct, const char* msg) { + if (progress_callback) { + progress_callback({pct, msg}); + } + }; + auto sub_report = [&](int sub_pct, int range_start, int range_end, const char* msg) { + int pct = range_start + sub_pct * (range_end - range_start) / 100; + report(pct, msg); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled"; + return true; + } + return false; + }; + + color_mesh.clear(); + face_colors.clear(); + + report(0, "Initializing"); + if (cancelled()) { + return false; + } + + if (texture_mesh.indices.size() == 0) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture mesh has no faces."; + return false; + } + if (texture.empty()) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture is empty."; + return false; + } + if (texture.channels() < 3) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture must have at least 3 channels, got " << texture.channels(); + return false; + } + if (texture_mesh_uv_coords.size() != texture_mesh.indices.size()) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: uv_coords size is not equal to texture mesh faces size."; + return false; + } + for (std::size_t fid = 0; fid < texture_mesh.indices.size(); ++fid) { + if (texture_mesh_uv_coords[fid].size() != 3) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: uv_coords of single face size is not equal to 3."; + return false; + } + } + color_mesh = texture_mesh; + + using Clock = std::chrono::high_resolution_clock; + const auto t_total_start = Clock::now(); + auto t_step = t_total_start; + auto lap = [&](const char* step_name) { + auto now = Clock::now(); + double ms = std::chrono::duration(now - t_step).count(); + BOOST_LOG_TRIVIAL(debug) << "[timing] " << step_name << ": " << ms << "ms" + << " faces=" << color_mesh.facets_count(); + t_step = now; + }; + + report(5, "Oversampling"); + if (cancelled()) { + return false; + } + + // Step 1: Oversampling (subdivision while propagating UVs) + // Convert external vector> to internal vector> to eliminate inner-level heap allocations + std::vector color_mesh_uv_coords(texture_mesh_uv_coords.size()); + for (std::size_t i = 0; i < texture_mesh_uv_coords.size(); ++i) { + color_mesh_uv_coords[i] = {texture_mesh_uv_coords[i][0], texture_mesh_uv_coords[i][1], texture_mesh_uv_coords[i][2]}; + } + { + // Estimate total iterations and map each iteration's sub-progress to the [5, 25] range + size_t estimated_iters = 0; + if (settings.oversampling_iters > 0) { + estimated_iters = settings.oversampling_iters; + } else { + size_t fc = color_mesh.facets_count(); + while (fc < settings.oversampling_min_face_count) { + fc *= 4; + ++estimated_iters; + } + if (estimated_iters == 0) { + estimated_iters = 1; + } + } + + auto make_iter_progress = [&](size_t iter) { + return [&, iter, estimated_iters](int pct) { + int iter_start = static_cast(iter * 100 / estimated_iters); + int iter_end = static_cast((iter + 1) * 100 / estimated_iters); + int sub_pct = iter_start + pct * (iter_end - iter_start) / 100; + sub_report(sub_pct, 5, 25, "Oversampling"); + }; + }; + + if (settings.oversampling_iters > 0) { + for (size_t i = 0; i < settings.oversampling_iters && color_mesh.facets_count() * 4.0 < settings.oversampling_max_face_count; ++i) { + if (cancelled()) return false; + linear_subdivision(color_mesh, color_mesh_uv_coords, make_iter_progress(i)); + } + } else { + size_t iter = 0; + while (color_mesh.facets_count() < settings.oversampling_min_face_count) { + if (cancelled()) return false; + linear_subdivision(color_mesh, color_mesh_uv_coords, make_iter_progress(iter++)); + } + } + } + + lap("Oversampling"); + + face_colors.resize(color_mesh.indices.size()); + + report(25, "Computing face colors"); + if (cancelled()) { + return false; + } + + // Step 2: Compute each face's color (7-point Gaussian quadrature + bilinear interpolation sampling) + { + std::atomic done_faces{0}; + std::atomic cancel_requested{false}; + const size_t total_faces = color_mesh.indices.size(); + const size_t report_interval = std::max(total_faces / 20, 1); + tbb::parallel_for(tbb::blocked_range(0, total_faces), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + if (cancel_requested.load(std::memory_order_relaxed)) return; + face_colors[fid] = sample_face_color(color_mesh_uv_coords[fid], texture); + size_t cnt = done_faces.fetch_add(1, std::memory_order_relaxed) + 1; + if (cnt % report_interval == 0) { + if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; } + sub_report(static_cast(cnt * 100 / total_faces), 25, 40, "Computing face colors"); + } + } + }); + if (cancel_requested.load() || cancelled()) return false; + } + lap("Computing face colors"); +#ifdef OUTPUT_TEST_RESULT + SaveToOFF("texture_to_color_0_initialize.off", color_mesh, face_colors); +#endif + + // Map progress from repair_cluster_smooth's [0,100] to TextureToColor's [40,100] + AlgoProgressCallback rcs_progress = nullptr; + if (progress_callback) { + rcs_progress = [&](AlgoProgress p) { + int mapped_pct = 40 + p.percent * 60 / 100; + progress_callback({mapped_pct, p.message}); + }; + } + + std::vector clustered_face_colors; + if (!repair_cluster_smooth(color_mesh, face_colors, clustered_face_colors, + settings, rcs_progress, cancel_callback, "TextureToColor")) + return false; + + face_colors = std::move(clustered_face_colors); + lap("Repair + Clustering + Smoothing"); + double total_ms = std::chrono::duration(Clock::now() - t_total_start).count(); + BOOST_LOG_TRIVIAL(debug) << "[timing] TextureToColor total: " << total_ms << "ms" + << " faces=" << color_mesh.facets_count(); + report(100, "Completed"); + return true; +} + +bool ClusterAndSmooth(const TriMesh& mesh, + const std::vector>& input_face_colors, + TriMesh& out_mesh, + std::vector>& out_face_colors, + const TextureToColorSettings& settings, + AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback, + const std::vector>& vertex_colors) +{ + auto report = [&](int pct, const char* msg) { + if (progress_callback) + progress_callback({pct, msg}); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled"; + return true; + } + return false; + }; + + out_mesh = mesh; + out_face_colors.clear(); + + if (mesh.indices.empty() || input_face_colors.empty()) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: empty mesh or face colors."; + return false; + } + if (input_face_colors.size() != mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "ClusterAndSmooth: face_colors size (" + << input_face_colors.size() << ") != indices size (" + << mesh.indices.size() << "), clamping."; + } + + report(0, "Initializing"); + if (cancelled()) return false; + + // Prepare face colors aligned to mesh size + std::vector face_colors(out_mesh.indices.size()); + for (size_t i = 0; i < out_mesh.indices.size(); ++i) { + if (i < input_face_colors.size()) + face_colors[i] = input_face_colors[i]; + else + face_colors[i] = {128, 128, 128}; + } + + // Low-poly vertex-color meshes take the legacy OBJ import route: quantize the + // vertex colors, then split only across cluster boundaries. Colors are exact + // cluster centers afterwards, so repair / re-clustering / smoothing are skipped + // to match the legacy behaviour, which never touched the mesh either. + // A vertex color count that disagrees with the mesh falls through to the generic + // pipeline below rather than failing the import outright. + if (!vertex_colors.empty() && + vertex_colors.size() == out_mesh.vertices.size() && + out_mesh.facets_count() < settings.oversampling_min_face_count) { + report(10, "Quantizing vertex colors"); + std::vector cluster_centers; + std::vector vertex_cluster_ids; + if (!quantize_vertex_colors(vertex_colors, settings, cancel_callback, cluster_centers, vertex_cluster_ids)) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: vertex color quantization failed."; + return false; + } + if (cancelled()) return false; + + report(50, "Splitting color boundaries"); + if (!adaptive_split_by_vertex_clusters(out_mesh, vertex_cluster_ids, cluster_centers, face_colors)) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: adaptive vertex-color split failed."; + return false; + } + if (cancelled()) return false; + + out_face_colors = std::move(face_colors); + report(100, "Completed"); + return true; + } + + std::vector clustered_face_colors; + if (!repair_cluster_smooth(out_mesh, face_colors, clustered_face_colors, + settings, progress_callback, cancel_callback, + "ClusterAndSmooth")) + return false; + + out_face_colors = std::move(clustered_face_colors); + return true; +} + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/TextureToColor.hpp b/src/libslic3r/TextureToColor/TextureToColor.hpp new file mode 100644 index 0000000000..019a113fc4 --- /dev/null +++ b/src/libslic3r/TextureToColor/TextureToColor.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "Callbacks.hpp" +#include "TriMesh.hpp" +#include "opencv2/core.hpp" +#include +#include + +namespace Slic3r { namespace tex2color { + +enum class MeshRepairDecision { + Ask, + ImportWithoutRepair, + RepairAndImport +}; + +using MeshRepairCallback = std::function progress_callback, + std::function 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>& uv_coords, const cv::Mat& texture, TriMesh& color_mesh, + std::vector>& 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>& input_face_colors, + TriMesh& out_mesh, + std::vector>& out_face_colors, + const TextureToColorSettings& settings = TextureToColorSettings(), + AlgoProgressCallback progress_callback = nullptr, + AlgoCancelCallback cancel_callback = nullptr, + const std::vector>& vertex_colors = {}); + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/TriMesh.hpp b/src/libslic3r/TextureToColor/TriMesh.hpp new file mode 100644 index 0000000000..d557ae1d2e --- /dev/null +++ b/src/libslic3r/TextureToColor/TriMesh.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include "Point.hpp" + +namespace Slic3r { namespace tex2color { + +using TriVertex = stl_vertex; +using TriVertices = std::vector; +using TriFace = stl_triangle_vertex_indices; +using TriFaces = std::vector; + +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 indices_, + std::vector vertices_) + : ::indexed_triangle_set(std::move(indices_), std::move(vertices_)) {} + + std::size_t facets_count() const { return indices.size(); } +}; + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index c403a6bd92..417ca354d3 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -146,6 +146,85 @@ public: using IntersectionLines = std::vector; +// 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; + +static CoplanarEdges coplanar_edges(const indexed_triangle_set &mesh, const std::vector &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 first_face(num_edges, -1); + std::vector first_face_edge(num_edges, -1); + std::vector face_planes(face_edge_ids.size()); + std::vector face_plane_computed(face_edge_ids.size(), false); + auto transformed_vertex = [&mesh, &trafo](int vertex_idx) { + return trafo * mesh.vertices[vertex_idx].cast(); + }; + // 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 &open_polylines) +static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, const CoplanarEdges &coplanar_edges, + Polygons &loops, std::vector &open_polylines) { // Build a map of lines by edge_a_id and a_id. std::vector 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 &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 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 static std::vector make_loops( // Lines will have their flags modified. std::vector &lines, + const CoplanarEdges &coplanar_edges, const MeshSlicingParams ¶ms, ThrowOnCancel throw_on_cancel) { @@ -1491,20 +1583,13 @@ static std::vector make_loops( layers.resize(lines.size()); tbb::parallel_for( tbb::blocked_range(0, lines.size()), - [&lines, &layers, ¶ms, throw_on_cancel](const tbb::blocked_range &range) { + [&lines, &layers, &coplanar_edges, ¶ms, throw_on_cancel](const tbb::blocked_range &range) { for (size_t line_idx = range.begin(); line_idx < range.end(); ++ line_idx) { if ((line_idx & 0x0ffff) == 0) throw_on_cancel(); Polygons &polygons = layers[line_idx]; - polygons = make_loops(lines[line_idx]); - - // Orca: A planar quad represented by two triangles contributes a point where the - // slicing plane crosses the shared diagonal. After rounding to coord_t this - // point may be very slightly off the otherwise straight contour edge. Apart - // from being redundant, such points make the subsequent contour - // simplification depend on the slice height (and may move seam candidates). - remove_collinear(polygons); + polygons = make_loops(lines[line_idx], coplanar_edges); auto this_mode = line_idx < params.slicing_mode_normal_below_layer ? params.mode_below : params.mode; if (! polygons.empty()) { @@ -1633,7 +1718,7 @@ static std::vector make_slab_loops( #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ Polygons &loops = layers[line_idx]; std::vector open_polylines; - chain_lines_by_triangle_connectivity(in, loops, open_polylines); + chain_lines_by_triangle_connectivity(in, {}, loops, open_polylines); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { SVG svg(debug_out_path("make_slab_loops-out-%d-%d-%s.svg", iRun, line_idx, ProjectionFromTop ? "top" : "bottom").c_str(), bbox_svg); @@ -1673,7 +1758,7 @@ static ExPolygons make_expolygons_simple(std::vector &lines) ExPolygons slices; Polygons holes; - for (Polygon &loop : make_loops(lines)) + for (Polygon &loop : make_loops(lines, {})) if (loop.area() >= 0.) slices.emplace_back(std::move(loop)); else @@ -1878,6 +1963,7 @@ std::vector slice_mesh( BOOST_LOG_TRIVIAL(debug) << "slice_mesh to polygons"; std::vector lines; + CoplanarEdges coplanar; { //FIXME facets_edges is likely not needed and quite costly to calculate. @@ -1885,6 +1971,8 @@ std::vector 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 face_edge_ids = its_face_edge_ids(mesh); + // Orca: Keep the coplanarity classification aligned with the edge IDs used to chain this slice. + coplanar = coplanar_edges(mesh, face_edge_ids, params.trafo); if (zs.size() <= 1) { // It likely is not worthwile to copy the vertices. Apply the transformation in place. if (is_identity(params.trafo)) { @@ -1906,7 +1994,7 @@ std::vector slice_mesh( throw_on_cancel(); - std::vector layers = make_loops(lines, params, throw_on_cancel); + std::vector layers = make_loops(lines, coplanar, params, throw_on_cancel); #ifdef SLIC3R_DEBUG { @@ -1952,6 +2040,7 @@ Polygons slice_mesh( const MeshSlicingParams ¶ms) { std::vector lines; + CoplanarEdges coplanar; { bool trafo_identity = is_identity(params.trafo); @@ -1987,6 +2076,8 @@ Polygons slice_mesh( // 3) Calculate face neighbors for just the faces in face_mask. std::vector face_edge_ids = its_face_edge_ids(mesh, face_mask); + // Orca: The single-plane path has its own masked edge-ID space, so classify that space separately. + coplanar = coplanar_edges(mesh, face_edge_ids, params.trafo); // 4) Slice "face_mask" triangles, collect line segments. // It likely is not worthwile to copy the vertices. Apply the transformation in place. @@ -2002,7 +2093,7 @@ Polygons slice_mesh( } // 5) Chain the line segments. - std::vector layers = make_loops(lines, params, [](){}); + std::vector layers = make_loops(lines, coplanar, params, [](){}); assert(layers.size() == 1); return layers.front(); } diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 3b032cc57e..b47004fca5 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1736,13 +1736,22 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const { data.used_states[n] = true; if (n >= 3) { - assert(n <= 16); - if (n <= 16) { - // Store "11" plus 4 bits of (n-3). - data.bitstream.insert(data.bitstream.end(), { true, true }); - n -= 3; + assert(n <= int(EnforcerBlockerType::ExtruderMax)); + // Store "11" plus 4 bits of (n-3), which covers states 3..17. State 18 and + // above set that nibble to 0b1111 and store (n-18) in a second nibble. This is + // the encoding the CONST_FILAMENTS table in Model.cpp already writes for + // colored mesh imports. + data.bitstream.insert(data.bitstream.end(), { true, true }); + auto &bitstream = data.bitstream; + auto push_nibble = [&bitstream](int value) { for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx) - data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx)); + bitstream.push_back(value & (uint64_t(0b0001) << bit_idx)); + }; + if (n <= 17) { + push_nibble(n - 3); + } else { + push_nibble(0b1111); + push_nibble(n - 18); } } else { // Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams. @@ -1810,6 +1819,12 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data, n |= data.bitstream[ibit ++] << i; return n; }; + // Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states + // 3..17, or 0b1111 followed by a nibble of (state-18) above that. + auto decode_leaf_state = [&next_nibble]() { + const int nibble = next_nibble(); + return EnforcerBlockerType(nibble == 0b1111 ? next_nibble() + 18 : nibble + 3); + }; parents.clear(); while (true) { @@ -1818,8 +1833,8 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data, int num_of_split_sides = code & 0b11; int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1; bool is_split = num_of_children != 0; - // Only valid if not is_split. Value of the second nibble was subtracted by 3, so it is added back. - auto state = is_split ? EnforcerBlockerType::NONE : EnforcerBlockerType((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2); + // Only valid if not is_split. + auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2)); // BBS if (state == to_delete_filament) @@ -1916,7 +1931,14 @@ void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bi if (const bool is_split = (code & 0b11) != 0; is_split) continue; - const uint8_t facet_state = (code & 0b1100) == 0b1100 ? read_next_nibble() + 3 : code >> 2; + uint8_t facet_state; + if ((code & 0b1100) == 0b1100) { + // Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18). + const uint8_t nibble = read_next_nibble(); + facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3); + } else { + facet_state = code >> 2; + } assert(facet_state < this->used_states.size()); if (facet_state >= this->used_states.size()) continue; @@ -1946,9 +1968,13 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor auto num_children_or_state = [&next_nibble]() -> int { int code = next_nibble(); int num_of_split_sides = code & 0b11; - return num_of_split_sides == 0 ? - ((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) : - - num_of_split_sides - 1; + if (num_of_split_sides != 0) + return - num_of_split_sides - 1; + if ((code & 0b1100) != 0b1100) + return code >> 2; + // Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18). + const int nibble = next_nibble(); + return nibble == 0b1111 ? next_nibble() + 18 : nibble + 3; }; int state = num_children_or_state(); @@ -1983,6 +2009,20 @@ void TriangleSelector::seed_fill_unselect_all_triangles() triangle.unselect_by_seed_fill(); } +void TriangleSelector::shift_states_above(EnforcerBlockerType threshold, int delta) +{ + for (Triangle &triangle : m_triangles) { + if (triangle.is_split() || !triangle.valid()) + continue; + EnforcerBlockerType s = triangle.get_state(); + if (s >= threshold && s != EnforcerBlockerType::NONE) { + int new_val = (int)s + delta; + if (new_val >= 0) + triangle.set_state(EnforcerBlockerType(new_val)); + } + } +} + void TriangleSelector::seed_fill_apply_on_triangles(EnforcerBlockerType new_state) { for (Triangle &triangle : m_triangles) diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 11517f5c6c..594f710e45 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -17,7 +17,9 @@ enum class EnforcerBlockerType : int8_t { BLOCKER = 2, // For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN). FUZZY_SKIN = ENFORCER, - // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. + // States 3..17 are serialized into 6 bits using a 2 bit prefix code; states 18 and above use + // one additional nibble (see TriangleSelector::serialize). ExtruderMax matches the last entry + // of CONST_FILAMENTS in Model.cpp, which encodes the same range for colored mesh imports. Extruder1 = ENFORCER, Extruder2 = BLOCKER, Extruder3, @@ -34,7 +36,23 @@ enum class EnforcerBlockerType : int8_t { Extruder14, Extruder15, Extruder16, - ExtruderMax = Extruder16 + Extruder17, + Extruder18, + Extruder19, + Extruder20, + Extruder21, + Extruder22, + Extruder23, + Extruder24, + Extruder25, + Extruder26, + Extruder27, + Extruder28, + Extruder29, + Extruder30, + Extruder31, + Extruder32, + ExtruderMax = Extruder32 }; // Type alias for the state mapping array to improve code readability @@ -369,6 +387,9 @@ public: // For all triangles, remove the flag indicating that the triangle was selected by seed fill. void seed_fill_unselect_all_triangles(); + // Shift all triangle states >= threshold by delta (used when inserting filaments) + void shift_states_above(EnforcerBlockerType threshold, int delta); + // For all triangles selected by seed fill, set new EnforcerBlockerType and remove flag indicating that triangle was selected by seed fill. // The operation may merge split triangles if they are being assigned the same color. void seed_fill_apply_on_triangles(EnforcerBlockerType new_state); diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index f4291d36df..6584566f40 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -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; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 35a8a375e2..6d505a231c 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -355,6 +355,16 @@ set(SLIC3R_GUI_SOURCES GUI/Monitor.hpp GUI/MonitorPage.cpp GUI/MonitorPage.hpp + GUI/MixedFilamentDialog.cpp + GUI/MixedFilamentDialog.hpp + GUI/GradientCurveEditor.cpp + GUI/GradientCurveEditor.hpp + GUI/ColorDecomposeDialog.cpp + GUI/ColorDecomposeDialog.hpp + GUI/ColorDecomposeSupport.cpp + GUI/ColorDecomposeSupport.hpp + GUI/TextureImportDialog.cpp + GUI/TextureImportDialog.hpp GUI/Mouse3DController.cpp GUI/Mouse3DController.hpp GUI/MsgDialog.cpp diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index bf5d1f2421..f65c0e3532 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -682,13 +682,19 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj if (shader) { if (idx == 0) { int extruder_id = model_volume->extruder_id(); - //to make black not too hard too see - ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]); - if (ban_light) { - new_color[3] = (255 - (extruder_id - 1))/255.0f; + // ORCA: extruder_id may be 0 (unset) or point past the colour list after a + // filament is deleted/remapped, so clamp the index instead of reading out of + // bounds. + if (!extruder_colors.empty()) { + int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1); + //to make black not too hard too see + ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]); + if (ban_light) { + new_color[3] = (255 - color_idx)/255.0f; + } + m.set_color(new_color); + // shader->set_uniform("uniform_color", new_color); } - m.set_color(new_color); - // shader->set_uniform("uniform_color", new_color); } else { if (idx <= extruder_colors.size()) { diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.hpp b/src/slic3r/GUI/CalibrationWizardSavePage.hpp index 4726cb1230..eb15720e96 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.hpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.hpp @@ -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; diff --git a/src/slic3r/GUI/CalibrationWizardStartPage.hpp b/src/slic3r/GUI/CalibrationWizardStartPage.hpp index 0e893bce10..026ce187ac 100644 --- a/src/slic3r/GUI/CalibrationWizardStartPage.hpp +++ b/src/slic3r/GUI/CalibrationWizardStartPage.hpp @@ -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; }; diff --git a/src/slic3r/GUI/ColorDecomposeDialog.cpp b/src/slic3r/GUI/ColorDecomposeDialog.cpp new file mode 100644 index 0000000000..2176de110e --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeDialog.cpp @@ -0,0 +1,951 @@ +#include "ColorDecomposeDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#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(mode); +} + +static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color) +{ + return { + static_cast(color.Red()), + static_cast(color.Green()), + static_cast(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(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& physical_colors, + const std::vector& filament_names, + const std::vector& filament_types, + size_t current_filament_count, + size_t max_filament_count, + std::vector 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(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 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(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(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 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 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(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(m_filament_idx) : size_t(-1); + const std::vector* 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 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(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& 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(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(std::clamp(r, 0.0, 255.0)), + static_cast(std::clamp(g, 0.0, 255.0)), + static_cast(std::clamp(b, 0.0, 255.0))); + return result; + }; + + std::vector 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(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(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 components; + for (size_t i = 0; i < std::min(2, physical_filaments.size()); ++i) { + DecomposeComponent comp; + comp.colour = wxColour(physical_filaments[i].color_hex); + comp.ratio = 50; + comp.filament_index = static_cast(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 diff --git a/src/slic3r/GUI/ColorDecomposeDialog.hpp b/src/slic3r/GUI/ColorDecomposeDialog.hpp new file mode 100644 index 0000000000..419dfe8cea --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeDialog.hpp @@ -0,0 +1,152 @@ +#ifndef slic3r_ColorDecomposeDialog_hpp_ +#define slic3r_ColorDecomposeDialog_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 components; +}; + +class ColorDecomposeDialog : public DPIDialog +{ +public: + ColorDecomposeDialog(wxWindow* parent, + int filament_idx, + const wxColour& target_color, + const std::vector& physical_colors, + const std::vector& filament_names, + const std::vector& filament_types, + size_t current_filament_count = 0, + size_t max_filament_count = 32, + std::vector 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 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 m_mode_results; + std::array m_mode_cards; + int m_filament_idx{-1}; + wxColour m_target_color; + std::vector m_physical_colors; + std::vector m_filament_names; + std::vector m_filament_types; + std::vector m_project_types; + std::string m_preferred_type; + // Dropdown selectable item index -> material type string + std::vector m_combo_item_types; + size_t m_current_filament_count{0}; + size_t m_max_filament_count{32}; + std::vector m_physical_config_indices; + std::function 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_ diff --git a/src/slic3r/GUI/ColorDecomposeSupport.cpp b/src/slic3r/GUI/ColorDecomposeSupport.cpp new file mode 100644 index 0000000000..e8fb4c9082 --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeSupport.cpp @@ -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 +#include +#include + +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(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& physical_types) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + if (auto* filament_id_opt = project_config.option("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("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("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 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()); + 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& physical_colors, + const std::vector& physical_config_indices, + size_t source_config_idx) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* filament_id_opt = project_config.option("filament_id"); + auto* type_opt = project_config.option("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(config_idx + 1); + } + + if (!expected_basic_type.empty() && (slot_type == expected_basic_type || slot_type == expected_short_type)) { + return static_cast(config_idx + 1); + } + + if (!expected_preset_part.empty() && preset_name.find(expected_preset_part) != std::string::npos) { + return static_cast(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(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& physical_colors, + const std::vector& physical_types, + const std::vector& physical_config_indices, + MixedFilamentResult& out_result, + std::vector& 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(comp.filament_index - 1); + if (physical_idx >= physical_config_indices.size()) { + return false; + } + out_result.components.push_back(static_cast(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(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& physical_colors, + const std::vector& physical_types, + size_t source_physical_idx, + const std::vector* physical_config_indices) +{ + if (result.mode != DecomposeMode::CMYW && result.mode != DecomposeMode::RYBW) + return 0; + + std::vector fallback_indices; + const std::vector* 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& 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 diff --git a/src/slic3r/GUI/ColorDecomposeSupport.hpp b/src/slic3r/GUI/ColorDecomposeSupport.hpp new file mode 100644 index 0000000000..a982c51303 --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeSupport.hpp @@ -0,0 +1,104 @@ +#ifndef slic3r_GUI_ColorDecomposeSupport_hpp_ +#define slic3r_GUI_ColorDecomposeSupport_hpp_ + +#include +#include +#include +#include +#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& 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& physical_colors, + const std::vector& 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& physical_colors, + const std::vector& physical_types, + const std::vector& physical_config_indices, + MixedFilamentResult& out_result, + std::vector& 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& physical_colors, + const std::vector& physical_types, + size_t source_physical_idx, + const std::vector* physical_config_indices); + +bool confirm_create_decompose_missing_components(wxWindow* parent, + const std::vector& missing); + +}} // namespace Slic3r::GUI + +#endif // slic3r_GUI_ColorDecomposeSupport_hpp_ diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index de94bb6b4b..d15164ef63 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -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(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("seam_slope_type") != SeamScarfType::None && diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index e57a569561..5d5d549427 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -385,7 +385,7 @@ public: wxWindow* window{ nullptr }; void BUILD() override; /// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER - void propagate_value() ; + void propagate_value() override; void set_value(const std::string& value, bool change_event = false) { m_disable_change_event = !change_event; @@ -440,7 +440,7 @@ public: wxWindow* window{ nullptr }; void BUILD() override; // Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER - void propagate_value(); + void propagate_value() override; /* Under OSX: wxBitmapComboBox->GetWindowStyle() returns some weard value, * so let use a flag, which has TRUE value for a control without wxCB_READONLY style diff --git a/src/slic3r/GUI/FilamentBitmapUtils.cpp b/src/slic3r/GUI/FilamentBitmapUtils.cpp index 9b43647ac0..1f51fc79b3 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.cpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.cpp @@ -4,7 +4,10 @@ #include #include "EncodedFilament.hpp" +#include "FilamentBitmapUtils.hpp" #include "GUI_App.hpp" +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/PrintConfig.hpp" namespace Slic3r { namespace GUI { @@ -28,6 +31,113 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from, } } +static std::string to_hex(const wxColour& c) +{ + return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString(); +} + +wxColour blend_n_colors(const std::vector& cols, const std::vector& weights) +{ + const size_t n = std::min(cols.size(), weights.size()); + std::vector hex_colors; + std::vector int_weights; + hex_colors.reserve(n); + int_weights.reserve(n); + for (size_t i = 0; i < n; ++i) { + hex_colors.push_back(to_hex(cols[i])); + // Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi; + // only relative magnitude matters. + int_weights.push_back(static_cast(std::lround(weights[i] * 10000.0))); + } + wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights)); + return blended.IsOk() ? blended : wxColour(128, 128, 128); +} + +std::vector sample_gradient_ramp(const wxColour& first, + const wxColour& second, + const Slic3r::GradientCurve& curve, + int steps) +{ + std::vector ramp; + if (steps <= 0 || curve.points.size() < 2) return ramp; + + ramp.reserve(steps); + for (int i = 0; i < steps; ++i) { + const double t = (steps > 1) ? (i + 0.5) / steps : 0.5; + const double r1 = Slic3r::sample_gradient_curve(curve, t); + ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1})); + } + return ramp; +} + +// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in +// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's +// endpoints, otherwise the 0.10 -> 0.90 default. +static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot) +{ + const auto* curve_opt = cfg.option("filament_mixed_gradient_curve"); + if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) { + Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]); + if (custom.points.size() >= 2) return custom; + } + + double start = kGradientMinRatio, end = kGradientMaxRatio; + const auto* range_opt = cfg.option("filament_mixed_gradient_range"); + if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 && + v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) { + start = v0; + end = v1; + } + } + + Slic3r::GradientCurve curve; + curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}}; + return curve; +} + +std::vector mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps) +{ + const auto* is_mixed_opt = cfg.option("filament_is_mixed"); + const auto* grad_opt = cfg.option("filament_mixed_gradient"); + const auto* comp_opt = cfg.option("filament_mixed_components"); + const auto* colour_opt = cfg.option("filament_colour"); + if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {}; + if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {}; + if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {}; + if (slot >= comp_opt->values.size()) return {}; + + // Only two-component slots fade; anything else stays on the plain blended swatch. + const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]); + if (comp_ids.size() != 2) return {}; + + auto component_colour = [&](unsigned int id) { + wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour(); + return c.IsOk() ? c : wxColour("#D9D9D9"); + }; + + // Both gradient_range and the curve express the *first* component's ratio over Z, so + // the components stay in config order and the curve alone decides which end is which. + return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]), + mixed_gradient_curve(cfg, slot), steps); +} + +void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector& ramp) +{ + if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return; + + dc.SetPen(*wxTRANSPARENT_PEN); + for (int y = 0; y < rect.height; ++y) { + // Row 0 is the top of the rect and so takes the ramp's last entry, the model's top. + // Mapping over height - 1 puts both ends of the ramp on screen even in a short swatch. + const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5; + dc.SetBrush(wxBrush(ramp[static_cast(t * (ramp.size() - 1) + 0.5)])); + dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1); + } +} + // Helper struct to hold bitmap and DC struct BitmapDC { wxBitmap bitmap; @@ -47,6 +157,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) { return BitmapDC(size); } +wxBitmap create_gradient_ramp_bitmap(const std::vector& ramp, const wxSize& size) +{ + if (ramp.empty()) return wxNullBitmap; + + BitmapDC bdc = init_bitmap_dc(size); + if (!bdc.dc.IsOk()) return wxNullBitmap; + + fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp); + + bdc.dc.SelectObject(wxNullBitmap); + return bdc.bitmap; +} + // Check if a color is transparent (alpha == 0) static bool is_transparent_color(const wxColour& color) { return color.Alpha() == 0; @@ -265,4 +388,65 @@ wxBitmap create_filament_bitmap(const std::vector& colors, const wxSiz } } +void recompute_mixed_slot_colors(std::vector& colors, + const Slic3r::DynamicPrintConfig& cfg) +{ + const auto* is_mixed_opt = cfg.option("filament_is_mixed"); + const auto* comp_opt = cfg.option("filament_mixed_components"); + const auto* ratio_opt = cfg.option("filament_mixed_sublayer_ratios"); + const auto* grad_opt = cfg.option("filament_mixed_gradient"); + if (!is_mixed_opt || !comp_opt) return; + + const size_t n = is_mixed_opt->values.size(); + if (colors.size() < n) colors.resize(n); + + const auto* colour_opt = cfg.option("filament_colour"); + const auto kFallback = wxColour(128, 128, 128, 255); + + for (size_t i = 0; i < n; ++i) { + if (!is_mixed_opt->values[i]) continue; + + if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; } + auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]); + if (comp_ids.empty()) { colors[i] = kFallback; continue; } + + bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i]; + std::vector use_ids = comp_ids; + std::vector weights; + + if (is_gradient && comp_ids.size() >= 2) { + use_ids = { comp_ids.front(), comp_ids.back() }; + weights = { 5000, 5000 }; + } else { + auto ratios_d = Slic3r::parse_mixed_ratios( + (ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{}, + comp_ids.size()); + weights.reserve(comp_ids.size()); + for (double r : ratios_d) + weights.push_back(static_cast(std::lround(r * 10000.0))); + } + + std::vector hex_colors; + hex_colors.reserve(use_ids.size()); + bool any_invalid = false; + for (unsigned int id : use_ids) { + if (id == 0 || id > colors.size()) { any_invalid = true; break; } + wxColour c = colors[id - 1]; + if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) { + hex_colors.push_back(to_hex(c)); + } else if (colour_opt && (id - 1) < colour_opt->values.size()) { + hex_colors.push_back(colour_opt->values[id - 1]); + } else { + any_invalid = true; break; + } + } + if (any_invalid) { colors[i] = kFallback; continue; } + + std::string hex = Slic3r::blend_color_multi(hex_colors, weights); + wxColour blended(hex); + if (!blended.IsOk()) blended = kFallback; + colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255); + } +} + }} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/FilamentBitmapUtils.hpp b/src/slic3r/GUI/FilamentBitmapUtils.hpp index 87d5b275cc..11696f3401 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.hpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.hpp @@ -7,6 +7,10 @@ #include #include +// Orca: forward-declare so the header is self-contained outside libslic3r_gui's +// force-included pch (the GUI test suite includes it directly). +namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; } + namespace Slic3r { namespace GUI { // Fills a rect with a west->east linear gradient by drawing solid 1px columns. @@ -28,6 +32,37 @@ wxBitmap create_filament_bitmap(const std::vector& colors, const wxSize& size, bool force_gradient = false); +// Blend colours at the given relative weights through blend_color_multi, so a measured +// real-world mix is used where one exists instead of a plain channel lerp. +wxColour blend_n_colors(const std::vector& cols, const std::vector& weights); + +// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the +// model's height, the curve gives the first component's ratio at t, and the two +// components are blended at that ratio through blend_n_colors. Entry 0 is the bottom +// of the model, the last entry its top. +std::vector sample_gradient_ramp(const wxColour& first, + const wxColour& second, + const Slic3r::GradientCurve& curve, + int steps); + +// Same ramp for a project config slot, resolving components, colours and curve (or the +// linear gradient_range fallback) from cfg. Returns empty for any slot that is not a +// two-component gradient mixed filament. steps is the ramp's resolution; pass the +// destination's height in pixels. +std::vector mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps); + +// Fill rect with a ramp, ramp.front() along the bottom edge. +void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector& ramp); + +// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp. +wxBitmap create_gradient_ramp_bitmap(const std::vector& ramp, const wxSize& size); + +// Recompute blended representative colors for mixed (virtual) filament slots. +// Reads mixed-filament config keys from cfg and writes back into colors[i] +// for every slot where filament_is_mixed[i] is true. +void recompute_mixed_slot_colors(std::vector& colors, + const Slic3r::DynamicPrintConfig& cfg); + }} // namespace Slic3r::GUI #endif // slic3r_GUI_FilamentBitmapUtils_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index fa6902de8d..34279369a1 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -155,6 +155,11 @@ std::string& get_filament_mixture_warning_text(){ return filament_mixture_warning_text; } +std::string& get_single_extruder_mixed_filament_warning_text(){ + static std::string single_extruder_mixed_filament_warning_text; + return single_extruder_mixed_filament_warning_text; +} + static std::string format_number(float value) { @@ -2984,6 +2989,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re bool mix_pla_and_petg = cur_plate->check_mixture_of_pla_and_petg(full_config_temp); _set_warning_notification(EWarning::MixUsePLAAndPETG, !mix_pla_and_petg); + bool single_extruder_mixed_risk = cur_plate->check_single_extruder_mixed_filament_risk(full_config_temp, get_single_extruder_mixed_filament_warning_text()); + _set_warning_notification(EWarning::SingleExtruderMixedFilament, single_extruder_mixed_risk); + bool filament_nozzle_compatible = cur_plate->check_compatible_of_nozzle_and_filament(full_config_temp, wxGetApp().preset_bundle->filament_presets, get_nozzle_filament_incompatible_text()); _set_warning_notification(EWarning::NozzleFilamentIncompatible, !filament_nozzle_compatible); @@ -3010,6 +3018,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re _set_warning_notification(EWarning::TPUPrintableError, false); _set_warning_notification(EWarning::FilamentPrintableError, false); _set_warning_notification(EWarning::MixUsePLAAndPETG, false); + _set_warning_notification(EWarning::SingleExtruderMixedFilament, false); _set_warning_notification(EWarning::PrimeTowerOutside, false); _set_warning_notification(EWarning::MultiExtruderPrintableError,false); _set_warning_notification(EWarning::MultiExtruderHeightOutside,false); @@ -8902,7 +8911,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED; } else { - if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice())) + // A plate using a mixed filament whose components are broken cannot be sliced, + // so surface that on the plate toolbar the same way an unsliceable plate is. + if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice()) + || wxGetApp().plater()->sidebar().has_broken_mixed_filament(plate_list.get_plate(i))) m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED; else { if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f) @@ -9669,6 +9681,13 @@ void GLCanvas3D::_render_paint_toolbar() const } } } + // ORCA: the loop above only labels a slot whose preset was found in the preset collection, + // while the render loop below iterates extruder_num. Pad the label arrays so a slot without a + // matching preset cannot index past them; a garbage std::string crashes ImGui::CalcTextSize. + while (int(filament_text_first_line.size()) < extruder_num) { + filament_text_first_line.emplace_back(); + filament_text_second_line.emplace_back(); + } ImGuiWrapper& imgui = *wxGetApp().imgui(); const float canvas_w = float(get_canvas_size().get_width()); @@ -9698,6 +9717,10 @@ void GLCanvas3D::_render_paint_toolbar() const bool disabled = !wxGetApp().plater()->can_fillcolor(); ColorRGBA rgba; + // Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than + // the single blended colour in `colors`. Every other slot's ramp is empty. + const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps(); + for (int i = 0; i < extruder_num; i++) { if (i > 0) ImGui::SameLine(); @@ -9711,6 +9734,8 @@ void GLCanvas3D::_render_paint_toolbar() const if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1)); } + if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) + ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]); if (ImGui::IsItemHovered() && i < 9) { if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale }); @@ -9726,7 +9751,13 @@ void GLCanvas3D::_render_paint_toolbar() const const float text_offset_y = 4.0f * em_unit * f_scale; for (int i = 0; i < extruder_num; i++) { - decode_color(colors[i], rgba); + // A gradient slot's swatch shows its fade instead of the blended colour in `colors`, so the + // labels take their contrast from the colour printed at the middle of the fade they sit on. + if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) { + const wxColour& c = gradient_ramps[i][gradient_ramps[i].size() / 2]; + rgba = ColorRGBA(c.Red(), c.Green(), c.Blue(), c.Alpha()); + } else + decode_color(colors[i], rgba); float gray = 0.299 * rgba.r_uchar() + 0.587 * rgba.g_uchar() + 0.114 * rgba.b_uchar(); ImVec4 text_color = gray < 80 ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0, 0, 0, 1.0f); @@ -10570,6 +10601,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) case EWarning::MixUsePLAAndPETG: text = _u8L("PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality."); break; + case EWarning::SingleExtruderMixedFilament: + text = get_single_extruder_mixed_filament_warning_text(); + break; case EWarning::PrimeTowerOutside: text = _u8L("The prime tower extends beyond the plate boundary."); break; @@ -10618,6 +10652,14 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) notification_manager.close_slicing_customize_error_notification(NotificationType::BBLNozzleFilamentIncompatible, NotificationLevel::WarningNotificationLevel); } } + else if (warning == EWarning::SingleExtruderMixedFilament) { + // Close by type: check_single_extruder_mixed_filament_risk() clears the shared text + // buffer on every call, so a close-by-text would miss once the risk is gone. + if (state) + notification_manager.push_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel, text); + else + notification_manager.close_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel); + } else { if (state) notification_manager.push_plater_warning_notification(text); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 17497edf16..84dbd5d652 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -391,6 +391,7 @@ class GLCanvas3D PrimeTowerOutside, NozzleFilamentIncompatible, MixtureFilamentIncompatible, + SingleExtruderMixedFilament, FlushingVolumeZero }; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 6028ada640..738af5e24c 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -521,10 +521,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_GCODE */ { L("G-code files"), { ".gcode"sv} }, #ifdef __APPLE__ /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, #else /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}}, #endif /* FT_ZIP */ { L("ZIP files"), { ".zip"sv } }, /* FT_PROJECT */ { L("Project files"), { ".3mf"sv} }, @@ -8905,7 +8905,11 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch if (printer_technology == ptFFF && !edited_printer_preset.config.opt_bool("single_extruder_multi_material")) { auto* nozzle_diameter = edited_printer_preset.config.option("nozzle_diameter"); if (nozzle_diameter) { - preset_bundle->set_num_filaments(nozzle_diameter->values.size()); + // Mixed-color slots are virtual filaments kept at the tail of the list, so they have no + // nozzle of their own. Sizing to the nozzle count alone would silently drop the mixes of + // a just-loaded project, and update_extruder_count() would then strip the facets painted + // with them. + preset_bundle->set_num_filaments(nozzle_diameter->values.size() + preset_bundle->num_mixed_filaments()); } } this->plater()->set_printer_technology(printer_technology); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 5254492e5d..be407a270f 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1656,16 +1656,16 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men { wxMenu *menu = &m_filament_action_menu; - if (init) { + // ORCA rebuild menu everytime instead checking existing of every item then deleting + while (menu->GetMenuItemCount() > 0) + menu->Destroy(menu->FindItemByPosition(0)); + + //if (init) { // append_menu_item( menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) { plater()->sidebar().edit_filament(); }, "", nullptr, []() { return true; }, m_parent); - } - - const int item_id = menu->FindItem(_L("Merge with")); - if (item_id != wxNOT_FOUND) - menu->Destroy(item_id); + //} wxMenu* sub_menu = new wxMenu(); std::vector icons = get_extruder_color_icons(true); @@ -1684,11 +1684,15 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "", [filaments_cnt]() { return filaments_cnt > 1; }, m_parent); + // Decompose a target colour into a printable mix of the loaded filaments. Placed before the + append_menu_item( + menu, wxID_ANY, _L("Decompose Color"), "", [](wxCommandEvent&) { + plater()->sidebar().decompose_filament_color(kSidebarContextMenuFilamentId); }, "", nullptr, + []() { return plater()->sidebar().combos_filament().size() >= 2; }, m_parent); + + menu->AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete + // ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS - const int delete_id = menu->FindItem(_L("Delete")); - if (delete_id != wxNOT_FOUND) - menu->Destroy(delete_id); - append_menu_item( menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) { plater()->sidebar().delete_filament(-2); }, "", nullptr, diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index dc89f5f9bb..b33cc82abc 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -3233,6 +3233,24 @@ void ObjectList::merge(bool to_multipart_object) void ObjectList::layers_editing() { + // Height ranges give each range its own layer height, varying the mixed sub-layer heights just + // like an adaptive profile, so this raises the same warning as variable layer height and shares + // its do-not-show-again flag. + const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (print_config.opt_bool("enable_mixed_color_sublayer")) { + if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") { + // Orca: parent to the plater like the sibling site in Plater::priv::on_action_layersediting + // (BBS passes nullptr, which MsgDialog remaps to the main frame). + MessageDialog dlg(wxGetApp().plater(), + _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), + _L("Warning"), wxICON_WARNING | wxOK); + dlg.show_dsa_button(); + dlg.ShowModal(); + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1"); + } + } + const Selection& selection = scene_selection(); const int obj_idx = selection.get_object_idx(); wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ? diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp index 4e531e6acc..cd3bc53cbd 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp @@ -85,7 +85,7 @@ public: void update_model_object(); //ClippingPlane get_sla_clipping_plane() const; - bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); } + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } bool wants_enter_leave_snapshots() const override { return true; } std::string get_gizmo_entering_text() const override { return _u8L("Entering Brim Ears"); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp index 9c36be5cd9..3ba613295d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp @@ -75,7 +75,7 @@ protected: virtual void on_render() override; virtual void on_set_state() override; virtual CommonGizmosDataID on_get_requirements() const override; - virtual void on_render_input_window(float x, float y, float bottom_limit); + virtual void on_render_input_window(float x, float y, float bottom_limit) override; void on_load(cereal::BinaryInputArchive &ar) override; void on_save(cereal::BinaryOutputArchive &ar) const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 761228550f..3d4af75cde 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -78,6 +78,9 @@ void GLGizmoMmuSegmentation::init_extruders_data() m_extruders_colors = wxGetApp().plater()->get_extruders_colors(); m_selected_extruder_idx = 0; + m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps(); + m_gradient_ramps.resize(m_extruders_colors.size()); + // keep remap table consistent with current extruder count m_extruder_remap.resize(m_extruders_colors.size()); for (size_t i = 0; i < m_extruder_remap.size(); ++i) @@ -305,15 +308,32 @@ void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y) } // ORCA -bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale) +bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale) { + // Inset of the frame stroked below, which is what trims the swatch down to its visible shape. + const float frame_inset = 1.5f; + ImDrawList* draw_list = ImGui::GetWindowDrawList(); std::string label_id = std::to_string(idx) + id_str + std::to_string(idx); ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 size = ImVec2(27.f * scale, 27.f * scale); ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color); ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1)); - bool dark_tone = (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51 + // Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade. + const std::vector* gradient = gradient_of(idx - 1); + // The centered slot number sits at the swatch's mid height, so take its contrast from the colour + // printed there rather than from the slot's blended color. + bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 : + (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51 + + // Paint a gradient mixed filament's fade before the button and keep the button transparent, so + // the slot number and the frame below stay on top of it. The bands cannot round their corners, + // so the fade is inset to the frame, which masks it into the shape a plain color slot gets. + if (gradient) { + ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale}, + {pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient); + color_vec.w = 0.f; // let the fade show through + } ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding , 7.f * scale); @@ -329,7 +349,7 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, cons auto drawBorder = [&](float d, float r, float t, ImU32 col) { draw_list->AddRect({pos.x + d * scale, pos.y + d * scale}, {pos.x + size.x - d * scale , pos.y + size.y - d * scale}, col, r * scale, 0, t * scale); }; - drawBorder(1.5f, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg))); + drawBorder(frame_inset, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg))); if(active) drawBorder(.5f, 4.f , 2.f, br_color); else @@ -433,7 +453,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_selected_extruder_idx = extruder_idx; } - if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); + if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); } // ORCA: Remap filaments section (Border only, Title in border). // Styled as a panel for visual grouping. @@ -731,6 +751,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors() continue; int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0; + // A volume may be assigned to a mixed-color slot, whose index can sit past the + // physical colour list; fall back to the first colour rather than reading OOB. + if (extruder_idx >= (int)m_extruders_colors.size()) + extruder_idx = 0; std::vector ebt_colors; ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]); ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end()); @@ -753,6 +777,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors() TriangleSelectorPatch* selector = dynamic_cast(m_triangle_selectors[i].get()); int extruder_idx = m_volumes_extruder_idxs[i]; int extruder_color_idx = std::max(0, extruder_idx - 1); + // A mixed-color slot can index past the physical colour list; fall back to the first colour. + if (extruder_color_idx >= (int)m_extruders_colors.size()) + extruder_color_idx = 0; std::vector ebt_colors; ebt_colors.push_back(m_extruders_colors[extruder_color_idx]); ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 55308bffa9..70cfde5aed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -73,11 +73,10 @@ public: void data_changed(bool is_serializing) override; - // TriangleSelector::serialization/deserialization has a limit to store 19 different states. - // EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored. - // When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization - // will be also extended to support additional states, requiring at least one state to remain free out of 19 states. - static const constexpr size_t EXTRUDERS_LIMIT = 16; + // The paint material limit follows EnforcerBlockerType::ExtruderMax: TriangleSelector + // serialization covers the extended (17..32) range through an escape nibble. Mixed-color + // filaments occupy ordinary slots, so they draw from the same budget as physical ones. + static const constexpr size_t EXTRUDERS_LIMIT = static_cast(EnforcerBlockerType::ExtruderMax); const float get_cursor_radius_min() const override { return CursorRadiusMin; } @@ -116,6 +115,10 @@ protected: // Filament remap feature std::vector m_extruder_remap; // index → target extruder index + // Colours each gradient mixed filament actually prints, bottom of the model first, mirrored + // from Plater so the extruder swatches draw the same fade the editor previews. Plain + // filament slots keep an empty ramp. + std::vector> m_gradient_ramps; // ORCA: Cache used filaments to filter UI std::set m_used_filaments; // Set of used filament indices (cached) @@ -137,7 +140,13 @@ private: void init_model_triangle_selectors(); // ORCA - bool draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale); + bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale); + // Gradient ramp of a filament slot, or nullptr when the slot is a plain single color + // filament. A non-null result is never empty. + const std::vector* gradient_of(int idx) const + { + return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr; + } // BBS void update_triangle_selectors_colors(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index df3abdddc7..fecc8abf1c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -67,7 +67,7 @@ protected: void on_register_raycasters_for_picking() override; void on_unregister_raycasters_for_picking() override; //BBS: GUI refactor: add object manipulation - virtual void on_render_input_window(float x, float y, float bottom_limit); + virtual void on_render_input_window(float x, float y, float bottom_limit) override; private: double calc_projection(const UpdateData& data) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index 6b46a596ba..3bfb63ff7a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -89,7 +89,7 @@ protected: virtual void on_register_raycasters_for_picking() override; virtual void on_unregister_raycasters_for_picking() override; //BBS: GUI refactor: add object manipulation - virtual void on_render_input_window(float x, float y, float bottom_limit); + virtual void on_render_input_window(float x, float y, float bottom_limit) override; private: void render_grabbers_connection(unsigned int id_1, unsigned int id_2, const ColorRGBA& color); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 94c76d896b..7882cf2269 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -998,16 +998,40 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) keyCode = keyCode- WXK_NUMPAD0+'0'; } if (keyCode >= '0' && keyCode <= '9') { - if (keyCode == '1' && !m_timer_set_color.IsRunning()) { + // The paint palette reaches EXTRUDERS_LIMIT slots (mixed-color filaments take + // ordinary slots too), so any leading digit that can start a valid two-digit + // number waits briefly for a second one. + const int digit = keyCode - '0'; + const int shortcut_max = int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT); + auto can_start_two_digit = [shortcut_max](int d) { return d > 0 && d * 10 <= shortcut_max; }; + auto select = [mmu_seg](int number) { return number > 0 && mmu_seg->on_number_key_down(number); }; + + if (m_timer_set_color.IsRunning() && m_pending_color_shortcut_tens > 0) { + const int two_digit = m_pending_color_shortcut_tens * 10 + digit; + const int pending = m_pending_color_shortcut_tens; + m_pending_color_shortcut_tens = 0; + m_timer_set_color.Stop(); + if (two_digit <= shortcut_max) { + processed = select(two_digit); + } else { + // Out of range: commit the pending digit, then treat this one as new input. + processed = select(pending); + if (can_start_two_digit(digit)) { + m_pending_color_shortcut_tens = digit; + m_timer_set_color.StartOnce(500); + processed = true; + } else { + processed = select(digit) || processed; + } + } + } + else if (can_start_two_digit(digit)) { + m_pending_color_shortcut_tens = digit; m_timer_set_color.StartOnce(500); processed = true; } - else if (keyCode < '7' && m_timer_set_color.IsRunning()) { - processed = mmu_seg->on_number_key_down(keyCode - '0'+10); - m_timer_set_color.Stop(); - } else { - processed = mmu_seg->on_number_key_down(keyCode - '0'); + processed = select(digit); } } else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') { @@ -1054,11 +1078,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) void GLGizmosManager::on_set_color_timer(wxTimerEvent& evt) { - if (m_current == MmSegmentation) { + // No second digit arrived in time: commit the pending leading digit on its own. + if (m_current == MmSegmentation && m_pending_color_shortcut_tens > 0) { GLGizmoMmuSegmentation* mmu_seg = dynamic_cast(get_current()); - mmu_seg->on_number_key_down(1); - m_parent.set_as_dirty(); + if (mmu_seg != nullptr) { + mmu_seg->on_number_key_down(m_pending_color_shortcut_tens); + m_parent.set_as_dirty(); + } } + m_pending_color_shortcut_tens = 0; } void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot) diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 01814521aa..157eb43dc7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -144,6 +144,8 @@ private: //When there are more than 9 colors, shortcut key coloring wxTimer m_timer_set_color; + // Leading digit of a two-digit color shortcut still waiting for its second digit. + int m_pending_color_shortcut_tens = 0; void on_set_color_timer(wxTimerEvent& evt); // key MENU_ICON_NAME, value = ImtextureID diff --git a/src/slic3r/GUI/GradientCurveEditor.cpp b/src/slic3r/GUI/GradientCurveEditor.cpp new file mode 100644 index 0000000000..5b1073d231 --- /dev/null +++ b/src/slic3r/GUI/GradientCurveEditor.cpp @@ -0,0 +1,656 @@ +#include "GradientCurveEditor.hpp" +#include "GUI_App.hpp" +#include "GuiColor.hpp" +#include "I18N.hpp" +#include "Widgets/StateColor.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); + +namespace { +// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing. +// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels. +constexpr double kPlotLeftRatio = 0.0316; +constexpr double kPlotRightRatio = 0.6766; +constexpr double kPlotTopRatio = 0.1529; +constexpr double kPlotBottomRatio = 0.8474; +constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders. + +// Hit / stroke (DIP). +constexpr int kHitRadius = 6; +constexpr int kCurveHitRadius = 5; +constexpr int kPointRadius = 4; // anchor outer radius (DIP) +constexpr int kStrokeUnselected = 2; +constexpr int kStrokeSelected = 4; +constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention) +constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP) +constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP) + +// Light-mode design tokens. Resolved through StateColor::darkModeColorFor() +// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B -> +// #818183, #262E30 -> #EFEFF0, #ACACAC -> #65656A, *wxWHITE -> #2D2D31). Don't read these +// directly in paint; always go through the resolved locals declared at the top of on_paint(). +const wxColour kGridColor (238, 238, 238); // #EEEEEE grey 300 +const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700 +const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700 +const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900 +const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements + +// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve +// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than +// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline. +constexpr float kBgSimilarThreshold = 15.0f; +constexpr int kOutlineExtraDip = 2; +} // namespace + +GradientCurveEditor::GradientCurveEditor(wxWindow* parent, + const wxColour& color_low, + const wxColour& color_high) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + , m_color_low(color_low) + , m_color_high(color_high) +{ + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetBackgroundColour(wxGetApp().get_window_default_clr()); + // Wide enough so the X-axis "Material Ratio" label fits past the arrow tip without overlap. + SetMinSize(FromDIP(wxSize(260, 200))); + + reset_to_linear(0.10, 0.90); + + Bind(wxEVT_PAINT, &GradientCurveEditor::on_paint, this); + Bind(wxEVT_LEFT_DOWN, &GradientCurveEditor::on_left_down, this); + Bind(wxEVT_LEFT_UP, &GradientCurveEditor::on_left_up, this); + Bind(wxEVT_RIGHT_DOWN, &GradientCurveEditor::on_right_down, this); + Bind(wxEVT_MOTION, &GradientCurveEditor::on_motion, this); + Bind(wxEVT_LEAVE_WINDOW,&GradientCurveEditor::on_leave, this); + Bind(wxEVT_SIZE, &GradientCurveEditor::on_size, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_drag_mode = DragMode::None; + m_drag_idx = -1; + m_dragged_moved = false; + }); +} + +GradientCurveEditor::~GradientCurveEditor() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); +} + +void GradientCurveEditor::set_points(const PointList& pts) +{ + m_points = pts; + normalize_points(); + Refresh(); +} + +void GradientCurveEditor::set_colors(const wxColour& color_low, const wxColour& color_high) +{ + m_color_low = color_low; + m_color_high = color_high; + Refresh(); +} + +void GradientCurveEditor::set_selected_curve(int curve_idx) +{ + const int new_sel = (curve_idx == 0) ? 0 : 1; + if (m_selected_curve == new_sel) return; + m_selected_curve = new_sel; + Refresh(); +} + +void GradientCurveEditor::reset_to_linear(double y0, double y1) +{ + auto clamp_y = [](double v) { + return std::max(kGradientMinRatio, std::min(kGradientMaxRatio, v)); + }; + m_points.clear(); + GradientAnchor a0; a0.x = 0.0; a0.y = clamp_y(y0); + GradientAnchor a1; a1.x = 1.0; a1.y = clamp_y(y1); + m_points.push_back(a0); + m_points.push_back(a1); + m_selected_curve = 0; + Refresh(); + emit_changed(); +} + +void GradientCurveEditor::reverse() +{ + // Mirror y around 0.5. Tangents are slopes dy/dx so they flip sign to keep the + // local shape consistent across the mirror; NaN tangents remain "use PCHIP default". + for (auto& p : m_points) { + p.y = 1.0 - p.y; + if (std::isfinite(p.m_in)) p.m_in = -p.m_in; + if (std::isfinite(p.m_out)) p.m_out = -p.m_out; + } + Refresh(); + emit_changed(); +} + +void GradientCurveEditor::normalize_points() +{ + if (m_points.empty()) { + GradientAnchor a0; a0.x = 0.0; a0.y = kGradientMinRatio; + GradientAnchor a1; a1.x = 1.0; a1.y = kGradientMaxRatio; + m_points.push_back(a0); + m_points.push_back(a1); + return; + } + + for (auto& p : m_points) { + p.x = std::max(0.0, std::min(1.0, p.x)); + p.y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, p.y)); + } + std::sort(m_points.begin(), m_points.end(), + [](const GradientAnchor& a, const GradientAnchor& b) { + return a.x < b.x; + }); + + if (m_points.size() < 2) { + GradientAnchor tail; tail.x = 1.0; tail.y = m_points.front().y; + m_points.push_back(tail); + } + + m_points.front().x = 0.0; + m_points.back().x = 1.0; +} + +void GradientCurveEditor::emit_changed() +{ + wxCommandEvent evt(wxEVT_GRADIENT_CURVE_CHANGED, GetId()); + evt.SetEventObject(this); + ProcessWindowEvent(evt); +} + +wxRect GradientCurveEditor::plot_rect() const +{ + const wxSize sz = GetClientSize(); + const int x = static_cast(std::lround(sz.x * kPlotLeftRatio)); + const int y = static_cast(std::lround(sz.y * kPlotTopRatio)); + const int x2 = static_cast(std::lround(sz.x * kPlotRightRatio)); + const int y2 = static_cast(std::lround(sz.y * kPlotBottomRatio)); + // Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at + // the top-left so the "100%" labels on the bottom/right still align with the plot edges. + const int side = std::max(1, std::min(x2 - x, y2 - y)); + return wxRect(x, y, side, side); +} + +wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const +{ + const wxRect r = plot_rect(); + // y axis is inverted: y=1 should sit at the top. + return wxPoint2DDouble(r.x + x * r.width, r.y + (1.0 - y) * r.height); +} + +wxPoint GradientCurveEditor::data_to_px(double x, double y) const +{ + const wxPoint2DDouble p = data_to_px_f(x, y); + return wxPoint(static_cast(std::lround(p.m_x)), static_cast(std::lround(p.m_y))); +} + +void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const +{ + const wxRect r = plot_rect(); + const double w = std::max(1, r.width); + const double h = std::max(1, r.height); + x = std::max(0.0, std::min(1.0, (px - r.x) / w)); + y = std::max(0.0, std::min(1.0, 1.0 - (py - r.y) / h)); +} + +double GradientCurveEditor::sample_curve_y(double x) const +{ + GradientCurve gc; + gc.points = m_points; + return sample_gradient_curve(gc, x); +} + +int GradientCurveEditor::hit_test(int px, int py) const +{ + const int tol = FromDIP(kHitRadius); + int best_idx = -1; + int best_d2 = tol * tol; + for (size_t i = 0; i < m_points.size(); ++i) { + // Anchor visual y is curve-specific: component 1's anchor sits at (x, 1 - stored_y). + const double vy = to_visual_y(m_selected_curve, m_points[i].y); + const wxPoint p = data_to_px(m_points[i].x, vy); + const int dx = px - p.x; + const int dy = py - p.y; + const int d2 = dx * dx + dy * dy; + if (d2 <= best_d2) { + best_idx = static_cast(i); + best_d2 = d2; + } + } + return best_idx; +} + +int GradientCurveEditor::hit_test_curve(int px, int py, int* seg_out) const +{ + if (seg_out) *seg_out = -1; + if (m_points.size() < 2) return -1; + const int tol = FromDIP(kCurveHitRadius); + const int tol2 = tol * tol; + + auto dist2_to_seg = [&](int ax, int ay, int bx, int by) -> int { + const double dx = bx - ax; + const double dy = by - ay; + const double l2 = dx * dx + dy * dy; + if (l2 == 0.0) { + const double ddx = px - ax; + const double ddy = py - ay; + return static_cast(ddx * ddx + ddy * ddy); + } + double t = ((px - ax) * dx + (py - ay) * dy) / l2; + t = std::max(0.0, std::min(1.0, t)); + const double ex = ax + t * dx; + const double ey = ay + t * dy; + const double ddx = px - ex; + const double ddy = py - ey; + return static_cast(ddx * ddx + ddy * ddy); + }; + + // Hit-test against the same dense Hermite polyline that on_paint draws, so the + // clickable line follows the visual curve exactly (no offset on the bent parts). + // When a hit is found, also report the index of the left anchor of the data-space + // segment that covers cursor x; needed by the segment-bend interaction. + const wxRect rc = plot_rect(); + const int samples = std::max(128, rc.width * 2); + auto seg_for_x = [&](double cursor_x) -> int { + for (size_t i = 1; i < m_points.size(); ++i) { + if (cursor_x <= m_points[i].x) + return static_cast(i - 1); + } + return static_cast(m_points.size() - 2); + }; + + auto curve_hit = [&](int curve_idx) -> bool { + wxPoint prev; + for (int s = 0; s <= samples; ++s) { + const double x = double(s) / samples; + const double y0 = sample_curve_y(x); + const double vy = to_visual_y(curve_idx, y0); + const wxPoint cur = data_to_px(x, vy); + if (s > 0 && dist2_to_seg(prev.x, prev.y, cur.x, cur.y) <= tol2) + return true; + prev = cur; + } + return false; + }; + + // Prefer the selected curve so overlapping segments don't unintentionally steal focus. + if (curve_hit(m_selected_curve)) { + if (seg_out) { + double nx = 0, dummy = 0; + px_to_data(px, py, nx, dummy); + *seg_out = seg_for_x(nx); + } + return m_selected_curve; + } + const int other = 1 - m_selected_curve; + if (curve_hit(other)) { + if (seg_out) { + double nx = 0, dummy = 0; + px_to_data(px, py, nx, dummy); + *seg_out = seg_for_x(nx); + } + return other; + } + return -1; +} + +void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) +{ + // Resolve theme colors every paint so dark-mode toggles (no re-construction) take + // effect without an explicit listener. Window bg is read from GUI_App, not + // GetBackgroundColour(), since the latter is snapshotted at construction time. + const wxColour bg = wxGetApp().get_window_default_clr(); + const wxColour grid_color = StateColor::darkModeColorFor(kGridColor); + const wxColour axis_color = StateColor::darkModeColorFor(kAxisColor); + const wxColour label_muted = StateColor::darkModeColorFor(kLabelMuted); + const wxColour label_strong = StateColor::darkModeColorFor(kLabelStrong); + const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE); + // Softer than axis_color: the curve outline only has to lift the curve off the + // background, it must not compete with the structural axis / grid. + const wxColour outline_color = StateColor::darkModeColorFor(kOutlineColor); + + wxAutoBufferedPaintDC raw_dc(this); + raw_dc.SetBackground(wxBrush(bg)); + raw_dc.Clear(); + + // Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered + // DC is the actual back buffer that gets blitted to the window. + wxGCDC dc(raw_dc); + // The curve and its anchors are drawn straight on the graphics context so their + // coordinates stay sub-pixel accurate (see data_to_px_f). + wxGraphicsContext* gc = dc.GetGraphicsContext(); + + const wxRect rc = plot_rect(); + if (rc.width <= 0 || rc.height <= 0) + return; + + // 10x10 light grid (10 lines including outer borders, 9 equal divisions). + dc.SetPen(wxPen(grid_color, 1)); + for (int i = 0; i <= kGridDivisions; ++i) { + const int x = rc.x + rc.width * i / kGridDivisions; + const int y = rc.y + rc.height * i / kGridDivisions; + dc.DrawLine(x, rc.y, x, rc.y + rc.height); + dc.DrawLine(rc.x, y, rc.x + rc.width, y); + } + + // Set the label font first so text width measurements drive arrow / label placement. + wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); + dc.SetFont(label_font); + + const wxString axis_y_title = _L("Material Ratio"); + const wxString axis_x_title = _L("Model Height"); + const wxString pct_text = wxT("100%"); + const wxSize x_title_sz = dc.GetTextExtent(axis_x_title); + const wxSize y_title_sz = dc.GetTextExtent(axis_y_title); + + wxFont strong_font = label_font; + strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD); + dc.SetFont(strong_font); + const wxSize pct_text_sz = dc.GetTextExtent(pct_text); + dc.SetFont(label_font); + + // Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the + // canvas top edge; X-axis extends past the plot right toward the canvas right edge. + const int arrow_half = FromDIP(kAxisArrowHalf); + const int arrow_len = FromDIP(kAxisArrowLen); + const wxSize sz = GetClientSize(); + dc.SetPen(wxPen(axis_color, kStrokeAxis)); + dc.SetBrush(wxBrush(axis_color)); + + // Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom. + const int y_axis_x = rc.x; + const int y_title_pct_gap = FromDIP(1); + const int y_title_bottom_pad = FromDIP(2); + const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad); + const int y_arrow_tip_y = y_title_y; + const int y_arrow_ty = y_arrow_tip_y + arrow_len; + dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height); + { + wxPoint tri[3] = { + wxPoint(y_axis_x, y_arrow_tip_y), + wxPoint(y_axis_x - arrow_half, y_arrow_ty), + wxPoint(y_axis_x + arrow_half, y_arrow_ty), + }; + dc.DrawPolygon(3, tri); + } + + // X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing + // "Material Ratio" label still fits inside the canvas without overlapping the arrow. + const int x_axis_y = rc.y + rc.height; + const int x_label_gap = FromDIP(4); + const int x_edge_pad = FromDIP(6); + const int x_arrow_ideal = rc.x + rc.width + FromDIP(10); + const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; + const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, + std::min(x_arrow_ideal, x_arrow_max)); + const int x_arrow_tip_x = x_arrow_tx + arrow_len; + const int x_title_x = x_arrow_tip_x + x_label_gap; + dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y); + { + wxPoint tri[3] = { + wxPoint(x_arrow_tip_x, x_axis_y), + wxPoint(x_arrow_tx, x_axis_y - arrow_half), + wxPoint(x_arrow_tx, x_axis_y + arrow_half), + }; + dc.DrawPolygon(3, tri); + } + + // Labels. + // "Model Height" and "100%" share the same left x; the gap is larger than the + // axis-arrow half-base so the text never visually touches the Y-axis arrow. + const int label_left_x = y_axis_x + FromDIP(10); + dc.SetTextForeground(label_muted); + dc.DrawText(axis_y_title, label_left_x, y_title_y); + + dc.SetFont(strong_font); + dc.SetTextForeground(label_strong); + dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap); + + // Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the + // X-axis arrow tip (placement was already clamped above to leave room). + dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y); + dc.SetFont(label_font); + dc.SetTextForeground(label_muted); + dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); + + if (m_points.size() < 2 || !gc) + return; + + auto color_for_curve = [&](int curve_idx) -> wxColour { + wxColour c = (curve_idx == 0) ? m_color_low : m_color_high; + // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible. + // Lift alpha so the curve stays visible while still hinting at transparency. + if (c.Alpha() == 0) + c.Set(c.Red(), c.Green(), c.Blue(), 150); + return c; + }; + + auto build_polyline = [&](int curve_idx) -> std::vector { + const int samples = std::max(128, rc.width * 2); + std::vector poly; + poly.reserve(samples + 1); + for (int s = 0; s <= samples; ++s) { + const double x = double(s) / samples; + const double y0 = sample_curve_y(x); + const double vy = to_visual_y(curve_idx, y0); + poly.push_back(data_to_px_f(x, vy)); + } + return poly; + }; + + // Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint + // and would quantize the curve back to whole pixels. The pen is still set on the dc, which + // forwards it here while keeping its own cached state in sync for later dc drawing. + auto draw_polyline = [&](const std::vector& poly, const wxColour& col, int stroke_dip) { + dc.SetPen(wxPen(col, FromDIP(stroke_dip))); + gc->StrokeLines(poly.size(), poly.data()); + }; + + // Outline only when the curve color is perceptually close to the background; otherwise + // the plain filament color reads fine and the extra stroke would look heavy. + auto needs_outline = [&](const wxColour& c) { + return calc_color_distance(c, bg) < kBgSimilarThreshold; + }; + + auto draw_one = [&](int curve_idx, int stroke_dip) { + const auto poly = build_polyline(curve_idx); + const wxColour col = color_for_curve(curve_idx); + if (needs_outline(col)) + draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip); + draw_polyline(poly, col, stroke_dip); + }; + + // Draw unselected first so the selected curve sits on top. + const int other = 1 - m_selected_curve; + draw_one(other, kStrokeUnselected); + draw_one(m_selected_curve, kStrokeSelected); + + // Control points (selected curve only): hollow circle with axis-color border, theme-aware fill. + // Drawn on the graphics context with a sub-pixel center so the ring stays centered on the + // curve instead of drifting up to half a pixel off it; pen and brush go through the dc for + // the same reason as in draw_polyline above. + const double r = FromDIP(kPointRadius); + dc.SetPen(wxPen(axis_color, 1)); + dc.SetBrush(wxBrush(point_fill)); + for (size_t i = 0; i < m_points.size(); ++i) { + const double vy = to_visual_y(m_selected_curve, m_points[i].y); + const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy); + gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); + } +} + +void GradientCurveEditor::on_left_down(wxMouseEvent& evt) +{ + const wxPoint pos = evt.GetPosition(); + m_dragged_moved = false; + + // 1) Anchor on the selected curve takes precedence over everything else. + // Dragging an anchor resets its tangent overrides so the surrounding curve + // returns to PCHIP-default shape (matches user expectation that pulling an + // anchor "straightens out" the local mess). + const int idx = hit_test(pos.x, pos.y); + if (idx >= 0) { + m_drag_mode = DragMode::Anchor; + m_drag_idx = idx; + // Only emit a change event when clearing the tangents actually mutates + // the curve. A plain click on an already-default anchor must not trigger + // re-slicing through the changed-event listener. + const bool had_tangent = std::isfinite(m_points[idx].m_in) + || std::isfinite(m_points[idx].m_out); + m_points[idx].m_in = std::numeric_limits::quiet_NaN(); + m_points[idx].m_out = std::numeric_limits::quiet_NaN(); + if (!HasCapture()) + CaptureMouse(); + Refresh(); + if (had_tangent) + emit_changed(); + return; + } + + // 2) Line-body hit. Determine which curve and which segment. + int seg = -1; + const int curve_hit = hit_test_curve(pos.x, pos.y, &seg); + if (curve_hit < 0) { + m_drag_mode = DragMode::None; + evt.Skip(); + return; + } + + // 3) Non-selected curve hit -> switch selection only, no drag arming. + if (curve_hit != m_selected_curve) { + m_selected_curve = curve_hit; + m_drag_mode = DragMode::None; + Refresh(); + evt.Skip(); + return; + } + + // 4) Selected curve line body hit -> insert a new anchor at cursor x (snapped + // to the current smooth curve so the initial click is visually invisible) + // and immediately enter Anchor drag mode. Bending the segment without + // inserting an anchor is not an option: a single cubic between two existing + // anchors cannot put its peak under an off-center cursor. + double nx = 0, dummy = 0; + px_to_data(pos.x, pos.y, nx, dummy); + if (nx <= 0.0 || nx >= 1.0 || seg < 0) { + m_drag_mode = DragMode::None; + evt.Skip(); + return; + } + GradientAnchor a; + a.x = nx; + a.y = sample_curve_y(nx); + const size_t insert_idx = static_cast(seg) + 1; + m_points.insert(m_points.begin() + insert_idx, a); + + m_drag_mode = DragMode::Anchor; + m_drag_idx = static_cast(insert_idx); + if (!HasCapture()) + CaptureMouse(); + Refresh(); + emit_changed(); +} + +void GradientCurveEditor::on_left_up(wxMouseEvent& evt) +{ + if (HasCapture()) + ReleaseMouse(); + + // Anchor mode (either an existing anchor or one freshly inserted by on_left_down) + // already fired emit_changed on mouse_down; only fire again here if the user + // actually dragged so the slicer doesn't re-run on a pure click. + if (m_drag_mode == DragMode::Anchor && m_dragged_moved) + emit_changed(); + + m_drag_mode = DragMode::None; + m_drag_idx = -1; + m_dragged_moved = false; + (void)evt; +} + +void GradientCurveEditor::on_right_down(wxMouseEvent& evt) +{ + const wxPoint pos = evt.GetPosition(); + const int idx = hit_test(pos.x, pos.y); + if (idx > 0 && static_cast(idx) + 1 < m_points.size()) { + // Interior anchor on the selected curve -> delete it. Endpoints stay locked. + m_points.erase(m_points.begin() + idx); + Refresh(); + emit_changed(); + return; + } + // Right-click on the non-selected curve switches selection (never deletes). + const int curve_hit = hit_test_curve(pos.x, pos.y); + if (curve_hit >= 0 && curve_hit != m_selected_curve) { + m_selected_curve = curve_hit; + Refresh(); + return; + } + evt.Skip(); +} + +void GradientCurveEditor::on_motion(wxMouseEvent& evt) +{ + if (!evt.LeftIsDown() || m_drag_mode != DragMode::Anchor) { + evt.Skip(); + return; + } + if (static_cast(m_drag_idx) >= m_points.size()) + return; + + const wxPoint pos = evt.GetPosition(); + double nx = 0, vy = 0; + px_to_data(pos.x, pos.y, nx, vy); + + auto& p = m_points[m_drag_idx]; + const bool is_first = (m_drag_idx == 0); + const bool is_last = (static_cast(m_drag_idx) + 1 == m_points.size()); + + // Endpoints stay locked at x=0 / x=1; interior anchors clamp into + // (left_neighbor.x, right_neighbor.x) so they can't cross or coincide. + if (!is_first && !is_last) { + const double xl = m_points[m_drag_idx - 1].x; + const double xr = m_points[m_drag_idx + 1].x; + const double eps = 1e-4; + nx = std::max(xl + eps, std::min(xr - eps, nx)); + p.x = nx; + } + // y is constrained to the reserved blend band so neither component ever + // reaches 0% / 100%, matching the sampler's clamp. + p.y = std::max(kGradientMinRatio, + std::min(kGradientMaxRatio, to_stored_y(m_selected_curve, vy))); + m_dragged_moved = true; + Refresh(); +} + +void GradientCurveEditor::on_leave(wxMouseEvent& evt) +{ + evt.Skip(); +} + +void GradientCurveEditor::on_size(wxSizeEvent& evt) +{ + Refresh(); + evt.Skip(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/GradientCurveEditor.hpp b/src/slic3r/GUI/GradientCurveEditor.hpp new file mode 100644 index 0000000000..f2e082aff5 --- /dev/null +++ b/src/slic3r/GUI/GradientCurveEditor.hpp @@ -0,0 +1,122 @@ +#ifndef slic3r_GradientCurveEditor_hpp_ +#define slic3r_GradientCurveEditor_hpp_ + +#include +#include +#include +#include +#include +#include + +#include "libslic3r/FilamentMixer.hpp" + +namespace Slic3r { +namespace GUI { + +// Photoshop-style curve editor for "Z progress -> first-component ratio" mapping. +// Curve evaluation uses cubic Hermite with PCHIP defaults plus optional per-anchor +// tangent overrides (m_in / m_out, NaN = use PCHIP default). The same evaluator +// (FilamentMixer::sample_gradient_curve) is shared with the slicing backend so what +// the editor renders matches the G-code output 1:1. +// +// Interaction model (PS Curves style): +// - Click or press-and-drag on the line body inserts a new anchor at the cursor x +// (snapped to the current smooth curve, NaN tangents) and starts dragging it. +// A pure click leaves an anchor sitting exactly on the previous curve shape; a +// drag moves the new anchor freely so the bump follows the cursor 1:1. +// - Dragging an existing anchor moves (x, y) and clears its m_in / m_out so the +// local curve returns to the PCHIP default shape around it. +// - Right-click on an interior anchor deletes it; endpoints stay locked. +class GradientCurveEditor : public wxPanel +{ +public: + using PointList = std::vector; + + GradientCurveEditor(wxWindow* parent, + const wxColour& color_low = wxColour(217, 217, 217), + const wxColour& color_high = wxColour(217, 217, 217)); + + ~GradientCurveEditor() override; + + // Replace the entire point list. The widget enforces x in [0,1], y in [0,1], + // sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are + // preserved as-is (NaN entries continue to use PCHIP defaults). + void set_points(const PointList& pts); + const PointList& get_points() const { return m_points; } + + void set_colors(const wxColour& color_low, const wxColour& color_high); + + // Which curve currently responds to drag / add / delete and is drawn with the thick stroke. + // 0 = first component (color_low), 1 = second component (color_high). Storage layer is + // unaffected: m_points always represents component 0's ratio. + void set_selected_curve(int curve_idx); + int get_selected_curve() const { return m_selected_curve; } + + // Reset to a two-point linear curve from y0 at t=0 to y1 at t=1. + // Clears all tangent overrides. + void reset_to_linear(double y0, double y1); + // Flip the curve top to bottom (all y -> 1 - y; tangents negated to mirror shape). + void reverse(); + +private: + enum class DragMode { + None, // nothing armed + Anchor, // dragging an anchor (either existing or just inserted from a line hit) + }; + + void normalize_points(); + void emit_changed(); + + void on_paint(wxPaintEvent& evt); + void on_left_down(wxMouseEvent& evt); + void on_left_up(wxMouseEvent& evt); + void on_right_down(wxMouseEvent& evt); + void on_motion(wxMouseEvent& evt); + void on_leave(wxMouseEvent& evt); + void on_size(wxSizeEvent& evt); + + // Coordinate mapping between data (x, y in [0,1]) and pixels in plot area. + wxRect plot_rect() const; + // Sub-pixel accurate mapping, used for drawing: rounding the curve vertices to whole + // pixels leaves a staircase that anti-aliasing cannot smooth out, and the step is + // twice as coarse on 2x (Retina) displays. + wxPoint2DDouble data_to_px_f(double x, double y) const; + wxPoint data_to_px(double x, double y) const; + void px_to_data(int px, int py, double& x, double& y) const; + // Anchor hit test for the currently-selected curve (uses translated visual y). + int hit_test(int px, int py) const; // returns point index or -1 + // Line-body hit test across both curves. Returns 0/1 for which curve was hit, -1 if none. + // Prefers the selected curve when both are within threshold. seg_out (when non-null) + // receives the left-anchor index of the segment that was hit on the returned curve; + // on_left_down uses it to know where in m_points to insert a freshly-added anchor. + int hit_test_curve(int px, int py, int* seg_out = nullptr) const; + + // Sample the curve in stored space (component 0) at x. + double sample_curve_y(double x) const; + + // Symmetric translation between visual y (what the user sees / clicks) and stored y + // (component 0's ratio in m_points). + static double to_stored_y(int curve_idx, double visual_y) { + return (curve_idx == 0) ? visual_y : (1.0 - visual_y); + } + static double to_visual_y(int curve_idx, double stored_y) { + return (curve_idx == 0) ? stored_y : (1.0 - stored_y); + } + + PointList m_points; + wxColour m_color_low; + wxColour m_color_high; + + int m_selected_curve = 0; + DragMode m_drag_mode = DragMode::None; + int m_drag_idx = -1; // valid when m_drag_mode == Anchor + bool m_dragged_moved = false; +}; + +// Custom event raised when the curve is edited (drag / add / remove / reset / reverse). +wxDECLARE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GradientCurveEditor_hpp_ diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 0d127f524b..5850161a3e 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -2404,6 +2404,25 @@ void ImGuiWrapper::draw( } } +void ImGuiWrapper::draw_gradient_ramp(ImDrawList *draw_list, const ImVec2 &top_left, const ImVec2 &bottom_right, const std::vector &ramp) +{ + if (draw_list == nullptr || ramp.empty() || bottom_right.x <= top_left.x || bottom_right.y <= top_left.y) + return; + + const int rows = std::max(1, (int) std::lround(bottom_right.y - top_left.y)); + const float row_h = (bottom_right.y - top_left.y) / rows; + const size_t last = ramp.size() - 1; + for (int r = 0; r < rows; ++r) { + // Row 0 is the top of the rect and so takes the ramp's last entry, the model's top. + const double t = (rows > 1) ? (double) (rows - 1 - r) / (rows - 1) : 0.5; + const wxColour &c = ramp[(size_t) (t * last + 0.5)]; + // The bottom row snaps to the rect's edge so rounding never leaves a sliver uncovered. + const float y0 = top_left.y + r * row_h; + const float y1 = (r + 1 == rows) ? bottom_right.y : top_left.y + (r + 1) * row_h; + draw_list->AddRectFilled({top_left.x, y0}, {bottom_right.x, y1}, IM_COL32(c.Red(), c.Green(), c.Blue(), c.Alpha())); + } +} + void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) { auto draw_list = ImGui::GetOverlayDrawList(); draw_list->AddCircle(position, radius, color, num_segments, thickness); diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index b586094ab3..db94b3edcb 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -3,10 +3,12 @@ #include #include +#include #include #include +#include #include #include "libslic3r/Point.hpp" @@ -299,6 +301,20 @@ public: int num_segments = 0, float thickness = 4.f); + /// + /// Fill a rect with a filament gradient ramp, one band per pixel row, ramp.front() along + /// the bottom edge. Bands rather than one interpolated rect, because the ramp follows the + /// slot's gradient curve and ImGui's corner interpolation could only draw a straight fade. + /// + /// Define where to draw it + /// Upper left corner of the rect + /// Lower right corner of the rect + /// Colours printed, bottom of the model first + static void draw_gradient_ramp(ImDrawList * draw_list, + const ImVec2 & top_left, + const ImVec2 & bottom_right, + const std::vector &ramp); + /// /// Check that font ranges contain all chars in string /// (rendered Unicodes are stored in GlyphRanges) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 752457d30f..3eb36154fc 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2341,6 +2341,11 @@ bool MainFrame::get_enable_slice_status() } } + // A mixed filament whose components were deleted, or whose components disagree in type, + // cannot be resolved at slicing time. Block the slice until the user fixes it. + if (enable && m_plater->sidebar().has_broken_mixed_filament()) + enable = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; return enable; } diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp new file mode 100644 index 0000000000..902c12d27b --- /dev/null +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -0,0 +1,1948 @@ +#include "MixedFilamentDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "libslic3r/FilamentMixer.hpp" +#include "I18N.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GradientCurveEditor.hpp" +#include "FilamentBitmapUtils.hpp" +#include "wxExtensions.hpp" +#include "Widgets/Button.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/ComboBox.hpp" +#include "Widgets/DropDown.hpp" +#include "Widgets/Label.hpp" + +namespace Slic3r { +namespace GUI { + +static constexpr int MAX_COMPONENTS = 3; +static constexpr int MIN_COMPONENT_RATIO = 10; + +// Section headings and the placeholder text share one muted tone; light key, resolved at each use. +static const wxColour COLOR_LABEL_MUTED("#6B6A6A"); + +// Lightweight self-painting label used for both dual-color and triple-color +// ratio percentage display. Hover shows a rounded-rect background; click +// fires wxEVT_LEFT_DOWN which the owning dialog binds to start_ratio_editor. +class RatioLabelPanel : public wxPanel +{ +public: + RatioLabelPanel(wxWindow* parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetCursor(wxCursor(wxCURSOR_HAND)); + SetToolTip(_L("Click to edit ratio")); + SetFont(::Label::Body_10); + + Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { m_hovered = true; Refresh(); e.Skip(); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { m_hovered = false; Refresh(); e.Skip(); }); + Bind(wxEVT_PAINT, &RatioLabelPanel::on_paint, this); + } + + void SetLabel(const wxString& text) override + { + if (m_text == text) return; + m_text = text; + update_best_size(); + Refresh(); + } + wxString GetLabel() const override { return m_text; } + +private: + void update_best_size() + { + wxClientDC dc(this); + dc.SetFont(GetFont()); + wxSize ts = dc.GetTextExtent(m_text); + int pad_x = FromDIP(4), pad_y = FromDIP(3); + SetMinSize(wxSize(ts.GetWidth() + pad_x * 2, ts.GetHeight() + pad_y * 2)); + InvalidateBestSize(); + } + + void on_paint(wxPaintEvent&) + { + wxBufferedPaintDC dc(this); + wxSize sz = GetClientSize(); + + wxColour parent_bg = GetParent() ? GetParent()->GetBackgroundColour() + : StateColor::darkModeColorFor(*wxWHITE); + dc.SetBrush(wxBrush(parent_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + if (m_hovered) { + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#F8F8F8")))); + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); + dc.DrawRoundedRectangle(0, 0, sz.GetWidth(), sz.GetHeight(), FromDIP(3)); + } + + dc.SetFont(GetFont()); + dc.SetTextForeground(m_hovered ? StateColor::darkModeColorFor(wxColour("#009688")) + : StateColor::darkModeColorFor(wxColour("#262E30"))); + wxSize ts = dc.GetTextExtent(m_text); + int x = (sz.GetWidth() - ts.GetWidth()) / 2; + int y = (sz.GetHeight() - ts.GetHeight()) / 2; + dc.DrawText(m_text, x, y); + } + + wxString m_text; + bool m_hovered{false}; +}; + +static wxColour blend_colors(const wxColour& a, const wxColour& b, double ratio_a) +{ + unsigned char r, g, bl; + Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(), + b.Red(), b.Green(), b.Blue(), + static_cast(1.0 - ratio_a), + &r, &g, &bl); + return wxColour(r, g, bl); +} + + +// ---- Constructors ---- + +MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, + const std::vector& physical_colors, + const std::vector& physical_names, + const std::vector& physical_types) + : DPIDialog(parent, wxID_ANY, _L("Add Mixed Filament"), wxDefaultPosition, + wxDefaultSize, wxCAPTION | wxCLOSE_BOX) + , m_edit_mode(false) + , m_physical_colors(physical_colors) + , m_physical_names(physical_names) + , m_physical_types(physical_types) +{ + m_result.components = {1, (physical_colors.size() >= 2) ? 2u : 1u}; + m_result.ratios = {50, 50}; + build_ui(); + wxGetApp().UpdateDlgDarkUI(this); +} + +MixedFilamentDialog::~MixedFilamentDialog() +{ + // Backstop: a child must never be destroyed while it still holds the mouse + // capture. wxWidgets only asserts about this (compiled out in release), and + // the macOS port never unwinds its capture stack, so the stale entry would + // make wxNSWindow::sendEvent swallow every mouse event in the application. + if (m_ratio_bar && m_ratio_bar->HasCapture()) + m_ratio_bar->ReleaseMouse(); + if (m_triangle_panel && m_triangle_panel->HasCapture()) + m_triangle_panel->ReleaseMouse(); +} + +MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, + const MixedFilamentResult& existing, + const std::vector& physical_colors, + const std::vector& physical_names, + const std::vector& physical_types) + : DPIDialog(parent, wxID_ANY, _L("Edit Mixed Filament"), wxDefaultPosition, + wxDefaultSize, wxCAPTION | wxCLOSE_BOX) + , m_result(existing) + , m_edit_mode(true) + , m_physical_colors(physical_colors) + , m_physical_names(physical_names) + , m_physical_types(physical_types) +{ + if (m_result.components.size() < 2) { + m_result.components = {1, (physical_colors.size() >= 2) ? 2u : 1u}; + m_result.ratios = {50, 50}; + } + if (m_result.ratios.size() >= 3) { + int sum = 0; + for (int r : m_result.ratios) sum += r; + if (sum > 0) { + m_tri_wx = (double)m_result.ratios[0] / sum; + m_tri_wy = (double)m_result.ratios[1] / sum; + m_tri_wz = (double)m_result.ratios[2] / sum; + } + } + build_ui(); + wxGetApp().UpdateDlgDarkUI(this); +} + +void MixedFilamentDialog::on_dpi_changed(const wxRect&) +{ + int h = (num_components() >= 3) ? FromDIP(680) : FromDIP(580); + SetSize(FromDIP(439), h); + Refresh(); +} + +wxColour MixedFilamentDialog::comp_colour(size_t i) const +{ + unsigned int c = comp(i); + if (c >= 1 && c <= m_physical_colors.size()) + return wxColour(m_physical_colors[c - 1]); + return wxColour("#D9D9D9"); +} + +static wxBitmap make_alpha_bitmap(int w, int h, + const std::function& draw_fn) +{ + wxBitmap bmp(w, h); + wxMemoryDC memdc; +#ifdef __WXOSX__ + bmp.UseAlpha(); + memdc.SelectObject(bmp); +#else + { + wxImage img(w, h); + img.InitAlpha(); + memset(img.GetAlpha(), 0, w * h); + bmp = wxBitmap(std::move(img)); + } + memdc.SelectObject(bmp); +#endif + { +#ifdef __WXMSW__ + wxGCDC dc(memdc); +#else + wxDC& dc = memdc; +#endif + draw_fn(dc); + } + memdc.SelectObject(wxNullBitmap); + return bmp; +} + +wxBitmap MixedFilamentDialog::make_swatch_bitmap(size_t idx) +{ + int swatch_sz = FromDIP(20); + int pad_left = FromDIP(2); + int pad_right = FromDIP(6); + int bmp_w = pad_left + swatch_sz + pad_right; + int bmp_h = swatch_sz; + + // Reuse the sidebar clr_picker swatch (get_extruder_color_icon) so the + // checkerboard (transparent.svg tiling), border and label style match the + // sidebar exactly, instead of a self-drawn rounded rect / programmatic grid. + std::string color_hex = "#D9D9D9"; + if (idx < m_physical_colors.size()) + color_hex = m_physical_colors[idx]; + std::string label = std::to_string(idx + 1); + + wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); + + return make_alpha_bitmap(bmp_w, bmp_h, [&](wxDC& dc) { + if (icon && icon->IsOk()) + dc.DrawBitmap(*icon, pad_left, 0); + }); +} + +void MixedFilamentDialog::apply_uniform_label_width(wxStaticText* lbl) +{ + // A material row places the combo right after the label, so the combo x follows the label + // width and the rows drift apart with fonts that render digits at different advances (which + // is what macOS does). Reserve the width of the widest row label on every row instead. + // The label itself is used as the measuring device on purpose: SetMinSize overrides the + // control's own best size rather than being merged with it, and on macOS the native cell is + // wider than the plain text extent, so a wxDC-measured width would clip the text. + const wxString text = lbl->GetLabel(); + int w = 0; + for (int i = 1; i <= MAX_COMPONENTS; ++i) { + lbl->SetLabel(wxString::Format(_L("Filament %d"), i)); + lbl->InvalidateBestSize(); + w = std::max(w, lbl->GetBestSize().x); + } + lbl->SetLabel(text); + lbl->InvalidateBestSize(); + lbl->SetMinSize(wxSize(w, -1)); +} + +void MixedFilamentDialog::append_material_row() +{ + auto* row = new wxBoxSizer(wxHORIZONTAL); + auto* lbl = new wxStaticText(this, wxID_ANY, + wxString::Format(_L("Filament %d"), (int)(m_combo_filaments.size() + 1))); + lbl->SetFont(::Label::Body_12); + apply_uniform_label_width(lbl); + row->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + auto* combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(166), FromDIP(24)), 0, nullptr, wxCB_READONLY); + combo->SetKeepDropArrow(true); + combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_filament_changed(); }); + row->Add(combo, 1, wxALIGN_CENTER_VERTICAL); + + m_combo_filaments.push_back(combo); + m_combo_to_physical.push_back({}); + m_material_rows_sizer->Add(row, 0, wxEXPAND | wxTOP, FromDIP(9)); +} + +void MixedFilamentDialog::reset_manual_ratio_state() +{ + m_ratio_manual_order.clear(); + if (m_ratio_editor_panel) + m_ratio_editor_panel->Hide(); + // Restore any label hidden by an in-flight editor so it can never be left + // permanently invisible if the editor is dismissed without a commit. + if (m_ratio_editor_anchor) { + m_ratio_editor_anchor->Show(); + m_ratio_editor_anchor = nullptr; + } +} + +void MixedFilamentDialog::refresh_ratio_labels() +{ + if (m_label_ratio_a) + m_label_ratio_a->SetLabel(wxString::Format(wxT("%d%%"), ratio(0))); + if (m_label_ratio_b) + m_label_ratio_b->SetLabel(wxString::Format(wxT("%d%%"), ratio(1))); + if (m_ratio_sizer) + m_ratio_sizer->Layout(); + if (m_triangle_panel) + m_triangle_panel->Refresh(); +} + +void MixedFilamentDialog::sync_triangle_weights_from_ratios() +{ + if (m_result.ratios.size() < 3) + return; + + int sum = 0; + for (int r : m_result.ratios) + sum += r; + if (sum <= 0) + return; + + m_tri_wx = (double)m_result.ratios[0] / sum; + m_tri_wy = (double)m_result.ratios[1] / sum; + m_tri_wz = (double)m_result.ratios[2] / sum; +} + +void MixedFilamentDialog::apply_manual_ratio(size_t idx, int value) +{ + const size_t n = num_components(); + if (idx >= n) + return; + if (m_result.ratios.size() != n) + m_result.ratios.assign(n, n > 0 ? 100 / (int)n : 0); + bool manual_stale = false; + for (size_t o : m_ratio_manual_order) { + if (o >= n) { manual_stale = true; break; } + } + if (manual_stale) + reset_manual_ratio_state(); + + int max_value = (int)(100 - (n - 1) * MIN_COMPONENT_RATIO); + value = std::clamp(value, MIN_COMPONENT_RATIO, std::max(MIN_COMPONENT_RATIO, max_value)); + + if (n == 2) { + if (idx == 0) { + m_result.ratios[0] = value; + m_result.ratios[1] = 100 - value; + } else { + m_result.ratios[1] = value; + m_result.ratios[0] = 100 - value; + } + m_result.ratios[0] = std::clamp(m_result.ratios[0], MIN_COMPONENT_RATIO, 100 - MIN_COMPONENT_RATIO); + m_result.ratios[1] = 100 - m_result.ratios[0]; + } else if (n >= 3) { + m_result.ratios[idx] = value; + int remaining = 100 - value; + + std::vector others; + int others_sum = 0; + for (size_t i = 0; i < n; ++i) { + if (i == idx) continue; + others.push_back(i); + others_sum += m_result.ratios[i]; + } + + if (!others.empty()) { + if (others_sum > 0) { + int assigned = 0; + for (size_t k = 0; k < others.size(); ++k) { + int nv = (int)((double)remaining * m_result.ratios[others[k]] / others_sum + 0.5); + nv = std::max(nv, MIN_COMPONENT_RATIO); + m_result.ratios[others[k]] = nv; + assigned += nv; + } + while (assigned != remaining) { + if (assigned > remaining) { + int pick = -1; + for (size_t k = 0; k < others.size(); ++k) + if (m_result.ratios[others[k]] > MIN_COMPONENT_RATIO + && (pick < 0 || m_result.ratios[others[k]] > m_result.ratios[others[pick]])) + pick = (int)k; + if (pick < 0) break; + --m_result.ratios[others[pick]]; --assigned; + } else { + int pick = 0; + for (size_t k = 1; k < others.size(); ++k) + if (m_result.ratios[others[k]] > m_result.ratios[others[pick]]) + pick = (int)k; + ++m_result.ratios[others[pick]]; ++assigned; + } + } + } else { + int base = remaining / (int)others.size(); + for (size_t k = 0; k < others.size(); ++k) + m_result.ratios[others[k]] = base; + m_result.ratios[others.back()] += remaining - base * (int)others.size(); + } + } + } + + refresh_ratio_labels(); + sync_triangle_weights_from_ratios(); + update_preview(); +} + +void MixedFilamentDialog::apply_dragged_triangle_ratio(int r0, int r1, int r2) +{ + if (m_result.ratios.size() < 3) + return; + + int ratios[3] = { + std::clamp(r0, MIN_COMPONENT_RATIO, 100), + std::clamp(r1, MIN_COMPONENT_RATIO, 100), + std::clamp(r2, MIN_COMPONENT_RATIO, 100) + }; + + int sum = ratios[0] + ratios[1] + ratios[2]; + while (sum > 100) { + int idx = 0; + for (int i = 1; i < 3; ++i) { + if (ratios[i] > ratios[idx]) + idx = i; + } + if (ratios[idx] <= MIN_COMPONENT_RATIO) + break; + --ratios[idx]; + --sum; + } + while (sum < 100) { + int idx = 0; + for (int i = 1; i < 3; ++i) { + if (ratios[i] < ratios[idx]) + idx = i; + } + ++ratios[idx]; + ++sum; + } + + m_result.ratios[0] = ratios[0]; + m_result.ratios[1] = ratios[1]; + m_result.ratios[2] = ratios[2]; + sync_triangle_weights_from_ratios(); + reset_manual_ratio_state(); + update_preview(); +} + +void MixedFilamentDialog::start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect) +{ + if (!anchor || idx >= m_result.ratios.size()) + return; + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) + commit_ratio_editor(true); + + if (!m_ratio_editor_panel) { + wxColour bg = StateColor::darkModeColorFor(wxColour("#F8F8F8")); + wxColour fg = StateColor::darkModeColorFor(wxColour("#262E30")); + + m_ratio_editor_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxBORDER_SIMPLE); + m_ratio_editor_panel->SetBackgroundColour(bg); + + auto* hsizer = new wxBoxSizer(wxHORIZONTAL); + + m_ratio_editor = new wxTextCtrl(m_ratio_editor_panel, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxDefaultSize, + wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_NONE); + m_ratio_editor->SetFont(::Label::Body_10); + m_ratio_editor->SetMaxLength(3); + m_ratio_editor->SetBackgroundColour(bg); + m_ratio_editor->SetForegroundColour(fg); + // Default wxTextCtrl best width (~140px) is too wide for the sizer to + // shrink, which would push the "%" suffix out of the panel. Size the + // editor for the *widest* three digits rather than the largest accepted + // value: SetMaxLength above lets anything up to "888" be typed, and the + // macOS system font renders digits at different advances, so "100" is + // narrower than what the user can actually enter. GetSizeFromTextSize() + // then adds the platform's own text field margins; on macOS those margins + // are what clipped the digits. + { + wxClientDC mdc(m_ratio_editor); + mdc.SetFont(::Label::Body_10); + int digits_w = mdc.GetTextExtent(wxT("888")).GetWidth(); + m_ratio_editor->SetMinSize(m_ratio_editor->GetSizeFromTextSize(digits_w)); + } + + auto* pct_label = new wxStaticText(m_ratio_editor_panel, wxID_ANY, wxT("%")); + pct_label->SetFont(::Label::Body_10); + pct_label->SetForegroundColour(fg); + pct_label->SetBackgroundColour(bg); + pct_label->SetMinSize(pct_label->GetBestSize()); + + hsizer->Add(m_ratio_editor, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + hsizer->Add(pct_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + m_ratio_editor_panel->SetSizer(hsizer); + m_ratio_editor_panel->Hide(); + + m_ratio_editor->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { commit_ratio_editor(true); }); + m_ratio_editor->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { + commit_ratio_editor(true); + e.Skip(); + }); + m_ratio_editor->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { + if (e.GetKeyCode() == WXK_ESCAPE) + commit_ratio_editor(false); + else + e.Skip(); + }); + } + + m_ratio_editor_idx = idx; + + // Keep the editor in the same window hierarchy as the clicked label so the + // z-order is reliable and the editor fully covers the anchor (dual-color + // labels live on the dialog, triple-color labels live on the triangle + // panel). + wxWindow* target_parent = anchor->GetParent(); + if (target_parent && m_ratio_editor_panel->GetParent() != target_parent) + m_ratio_editor_panel->Reparent(target_parent); + + // Hide the label being edited to avoid its (hover-state) text leaking out + // next to the editor; restored on commit. + m_ratio_editor_anchor = anchor; + anchor->Hide(); + + wxPoint pos = anchor->GetPosition() + anchor_rect.GetTopLeft(); + // Match the editor to the label (hover box) size so the inline editor and + // the hover state look identical, but never go below what the digits and + // the "%" suffix need: the sizer takes any missing width out of the + // stretchable editor, which would clip the value. + wxSize needed = m_ratio_editor_panel->ClientToWindowSize( + m_ratio_editor_panel->GetSizer()->CalcMin()); + wxSize size = anchor->GetSize(); + size.SetWidth(std::max(size.GetWidth(), needed.GetWidth())); + size.SetHeight(std::max(size.GetHeight(), needed.GetHeight())); + // An editor wider than the label must still stay inside its parent, or the + // corner labels of the triangle picker would have it clipped at the edge. + if (wxWindow* editor_parent = m_ratio_editor_panel->GetParent()) { + wxSize avail = editor_parent->GetClientSize(); + pos.x = std::clamp(pos.x, 0, std::max(0, avail.GetWidth() - size.GetWidth())); + pos.y = std::clamp(pos.y, 0, std::max(0, avail.GetHeight() - size.GetHeight())); + } + m_ratio_editor_panel->SetSize(wxRect(pos, size)); + m_ratio_editor_panel->Layout(); + m_ratio_editor->SetValue(wxString::Format(wxT("%d"), ratio(idx))); + m_ratio_editor_panel->Show(); + m_ratio_editor_panel->Raise(); + m_ratio_editor->SetFocus(); + m_ratio_editor->SelectAll(); + m_ratio_editor_panel->Refresh(); + Update(); +} + +void MixedFilamentDialog::commit_ratio_editor(bool apply) +{ + if (!m_ratio_editor_panel || !m_ratio_editor_panel->IsShown() || m_ratio_editor_committing) + return; + + m_ratio_editor_committing = true; + + // Restore the hidden anchor before applying the ratio, so any sizer layout + // triggered by refresh_ratio_labels() accounts for the visible label. + m_ratio_editor_panel->Hide(); + if (m_ratio_editor_anchor) { + m_ratio_editor_anchor->Show(); + m_ratio_editor_anchor = nullptr; + } + + if (apply) { + wxString value = m_ratio_editor->GetValue(); + value.Trim(true); + value.Trim(false); + if (value.EndsWith(wxT("%"))) + value.RemoveLast(); + + long parsed = 0; + if (value.ToLong(&parsed)) + apply_manual_ratio(m_ratio_editor_idx, (int)parsed); + else + refresh_ratio_labels(); + } + + m_ratio_editor_committing = false; +} + +void MixedFilamentDialog::commit_ratio_editor_from_background(wxMouseEvent& e) +{ + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) { + wxPoint mouse_in_panel = m_ratio_editor_panel->ScreenToClient(wxGetMousePosition()); + if (!m_ratio_editor_panel->GetClientRect().Contains(mouse_in_panel)) + commit_ratio_editor(true); + } + e.Skip(); +} + +// ---- UI Construction ---- + +void MixedFilamentDialog::build_ui() +{ + SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + Bind(wxEVT_LEFT_DOWN, &MixedFilamentDialog::commit_ratio_editor_from_background, this); + SetSize(FromDIP(439), FromDIP(580)); + + auto* main_sizer = new wxBoxSizer(wxVERTICAL); + + auto* top_sizer = new wxBoxSizer(wxHORIZONTAL); + top_sizer->Add(create_preview_panel(), 0, wxALL, FromDIP(20)); + + m_right_sizer = new wxBoxSizer(wxVERTICAL); + m_right_sizer->Add(create_material_selection(), 0, wxEXPAND); + m_right_sizer->Add(create_gradient_section(), 0, wxEXPAND | wxTOP, FromDIP(7)); + + m_ratio_sizer = create_ratio_slider(); + m_right_sizer->Add(m_ratio_sizer, 0, wxEXPAND | wxTOP, FromDIP(7)); + + m_triangle_sizer = create_triangle_picker(); + m_right_sizer->Add(m_triangle_sizer, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(7)); + + top_sizer->Add(m_right_sizer, 1, wxTOP | wxRIGHT | wxBOTTOM, FromDIP(20)); + main_sizer->Add(top_sizer, 0, wxEXPAND); + + main_sizer->Add(create_recommendation_grid(), 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(25)); + + // Warning panel: red bordered box with exclamation icon + text + m_warning_sizer = new wxBoxSizer(wxVERTICAL); + m_warning_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(48))); + m_warning_panel->SetMinSize(wxSize(-1, FromDIP(48))); + m_warning_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + m_warning_panel->Bind(wxEVT_PAINT, &MixedFilamentDialog::paint_warning_panel, this); + m_warning_sizer->Add(m_warning_panel, 0, wxEXPAND); + main_sizer->Add(m_warning_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(25)); + m_warning_panel->Hide(); + + main_sizer->Add(create_button_panel(), 0, wxALIGN_RIGHT | wxALL, FromDIP(20)); + + SetSizer(main_sizer); + + rebuild_all_combos(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + + Layout(); + CentreOnParent(); +} + +wxBoxSizer* MixedFilamentDialog::create_preview_panel() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + m_preview_canvas = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(129), FromDIP(129))); + m_preview_canvas->SetBackgroundStyle(wxBG_STYLE_PAINT); + + m_preview_canvas->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxBufferedPaintDC dc(m_preview_canvas); + wxSize sz = m_preview_canvas->GetClientSize(); + size_t n = num_components(); + + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + if (n == 0) return; + + int swatch_sz = FromDIP(80); + int x0 = (sz.GetWidth() - swatch_sz) / 2; + int y0 = (sz.GetHeight() - swatch_sz) / 2; + double radius = FromDIP(6); + + if (m_result.gradient_enabled && n == 2) { + Slic3r::GradientCurve curve; + if (!m_result.gradient_curve.empty()) { + curve.points = m_result.gradient_curve; + } else { + double yStart = (m_result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; + double yEnd = (m_result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; + curve.points = {{0.0, yStart, NAN, NAN}, {1.0, yEnd, NAN, NAN}}; + } + + // Same sampler the sidebar, extruder icons and paint gizmo swatches use, so this + // preview and every swatch drawn for the filament agree on what it looks like. + auto ramp = sample_gradient_ramp(comp_colour(0), comp_colour(1), curve, std::max(80, swatch_sz)); + fill_gradient_ramp_rect(dc, wxRect(x0, y0, swatch_sz, swatch_sz), ramp); + + // Mask corners: overdraw a thick background-colored rounded rect frame + // so the inner edge forms the desired rounded corners. + // Known limitation: this assumes the panel background equals + // darkModeColorFor(white). wxGraphicsContext::Clip(path) is not + // available in our wxWidgets build (only Clip(wxRegion) exists). + int r = static_cast(radius); + wxColour bg = StateColor::darkModeColorFor(*wxWHITE); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(wxPen(bg, r * 2)); + dc.DrawRoundedRectangle(x0 - r, y0 - r, swatch_sz + r * 2, swatch_sz + r * 2, radius * 2); + } else { + std::vector cols; + std::vector weights; + for (size_t i = 0; i < n; ++i) { + cols.push_back(comp_colour(i)); + weights.push_back(ratio(i) / 100.0); + } + wxColour mixed = blend_n_colors(cols, weights); + dc.SetBrush(wxBrush(mixed)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRoundedRectangle(x0, y0, swatch_sz, swatch_sz, radius); + } + }); + + sizer->Add(m_preview_canvas, 0, wxALIGN_CENTER); + + auto* label = new wxStaticText(this, wxID_ANY, _L("Effect Preview")); + label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + label->SetFont(::Label::Body_13); + sizer->Add(label, 0, wxALIGN_CENTER | wxTOP, FromDIP(4)); + + return sizer; +} + +wxBoxSizer* MixedFilamentDialog::create_material_selection() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + // Summary panel — draws N components dynamically + m_summary_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(234), FromDIP(40))); + m_summary_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + m_summary_panel->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxBufferedPaintDC dc(m_summary_panel); + wxSize sz = m_summary_panel->GetClientSize(); + + wxColour sum_bg = StateColor::darkModeColorFor(wxColour("#F8F8F8")); + wxColour sum_text = StateColor::darkModeColorFor(wxColour("#262E30")); + dc.SetBrush(wxBrush(sum_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + int swatch_sz = FromDIP(20); + int y_center = (sz.GetHeight() - swatch_sz) / 2; + int x = FromDIP(13); + + dc.SetFont(::Label::Body_13); + + auto draw_summary_swatch = [&](size_t comp_idx) { + unsigned int c = comp(comp_idx); + std::string color_hex = "#D9D9D9"; + if (c >= 1 && c <= m_physical_colors.size()) + color_hex = m_physical_colors[c - 1]; + std::string label = std::to_string(c); + wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); + if (icon && icon->IsOk()) + dc.DrawBitmap(*icon, x, y_center); + x += swatch_sz + FromDIP(4); + }; + + if (m_result.gradient_enabled && num_components() == 2) { + size_t idx_a = (m_result.gradient_direction == 0) ? 0 : 1; + size_t idx_b = 1 - idx_a; + draw_summary_swatch(idx_a); + + dc.SetTextForeground(sum_text); + wxString arrow = wxT("\u2192"); + wxSize arrow_sz = dc.GetTextExtent(arrow); + dc.DrawText(arrow, x, y_center + (swatch_sz - arrow_sz.GetHeight()) / 2); + x += arrow_sz.GetWidth() + FromDIP(4); + + draw_summary_swatch(idx_b); + } else { + for (size_t i = 0; i < num_components(); ++i) { + if (i > 0) { + dc.SetTextForeground(sum_text); + wxString plus = wxT("+"); + wxSize plus_sz = dc.GetTextExtent(plus); + dc.DrawText(plus, x, y_center + (swatch_sz - plus_sz.GetHeight()) / 2); + x += plus_sz.GetWidth() + FromDIP(4); + } + draw_summary_swatch(i); + + dc.SetTextForeground(sum_text); + wxString pct = wxString::Format(wxT("%d%%"), ratio(i)); + wxSize pct_sz = dc.GetTextExtent(pct); + dc.DrawText(pct, x, y_center + (swatch_sz - pct_sz.GetHeight()) / 2); + x += pct_sz.GetWidth() + FromDIP(4); + } + } + }); + sizer->Add(m_summary_panel, 0, wxEXPAND); + + auto* sel_label = new wxStaticText(this, wxID_ANY, _L("Select Mixed Materials")); + sel_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + sel_label->SetFont(::Label::Body_12); + sizer->Add(sel_label, 0, wxTOP, FromDIP(6)); + + m_material_rows_sizer = new wxBoxSizer(wxVERTICAL); + + m_combo_filaments.clear(); + m_combo_to_physical.clear(); + for (size_t i = 0; i < m_result.components.size(); ++i) + append_material_row(); + + sizer->Add(m_material_rows_sizer, 0, wxEXPAND); + + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_btn_add_material = new Button(this, _L("+ Add Material")); + m_btn_add_material->SetBackgroundColor(wxColour("#F8F8F8")); + m_btn_add_material->SetBorderColor(wxColour("#EEEEEE")); + // The disabled tone rides on the StateColor so Enable() alone repaints it, the way m_btn_ok does. + m_btn_add_material->SetTextColor(StateColor( + std::make_pair(wxColour("#ACACAC"), (int) StateColor::Disabled), + std::make_pair(wxColour("#262E30"), (int) StateColor::Normal))); + m_btn_add_material->SetMinSize(wxSize(-1, FromDIP(24))); + m_btn_add_material->SetCursor(wxCursor(wxCURSOR_HAND)); + m_btn_add_material->EnableTooltipEvenDisabled(); + m_btn_add_material->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_material(); }); + btn_sizer->Add(m_btn_add_material, 1, wxRIGHT, FromDIP(6)); + + m_btn_remove_material = new Button(this, _L("- Delete Material")); + m_btn_remove_material->SetBackgroundColor(wxColour("#F8F8F8")); + m_btn_remove_material->SetBorderColor(wxColour("#EEEEEE")); + m_btn_remove_material->SetTextColor(wxColour("#262E30")); + m_btn_remove_material->SetMinSize(wxSize(-1, FromDIP(24))); + m_btn_remove_material->SetCursor(wxCursor(wxCURSOR_HAND)); + m_btn_remove_material->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_remove_material(); }); + m_btn_remove_material->Hide(); + btn_sizer->Add(m_btn_remove_material, 1, 0, 0); + + sizer->Add(btn_sizer, 0, wxEXPAND | wxTOP, FromDIP(9)); + + return sizer; +} + +wxBoxSizer* MixedFilamentDialog::create_ratio_slider() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* ratio_label = new wxStaticText(this, wxID_ANY, _L("Ratio")); + ratio_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + ratio_label->SetFont(::Label::Body_12); + sizer->Add(ratio_label, 0, wxBOTTOM, FromDIP(4)); + + m_ratio_bar = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(27))); + m_ratio_bar->SetMinSize(wxSize(-1, FromDIP(27))); + m_ratio_bar->SetBackgroundStyle(wxBG_STYLE_PAINT); + + m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxBufferedPaintDC dc(m_ratio_bar); + wxSize sz = m_ratio_bar->GetClientSize(); + + wxColour col_a = comp_colour(0), col_b = comp_colour(1); + + for (int x = 0; x < sz.GetWidth(); ++x) { + double t = (double)x / sz.GetWidth(); + wxColour c = blend_colors(col_a, col_b, 1.0 - t); + dc.SetPen(wxPen(c)); + dc.DrawLine(x, 0, x, sz.GetHeight()); + } + + int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth()); + // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over + // blended filament colour, so it has to keep its contrast against data rather than chrome. + dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4))); + dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); + dc.SetPen(wxPen(*wxWHITE, FromDIP(2))); + dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); + }); + + m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) + commit_ratio_editor(true); + m_ratio_dragging = true; + if (!m_ratio_bar->HasCapture()) + m_ratio_bar->CaptureMouse(); + int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); + on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); + }); + + m_ratio_bar->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) { + if (!m_ratio_dragging) return; + int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); + on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); + }); + + // Key the release off the capture itself, not off the drag flag: the two can fall out of + // sync (a lost capture clears the flag on its own), and a capture that outlives the widget + // wedges mouse input for the whole application. + m_ratio_bar->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { + m_ratio_dragging = false; + if (m_ratio_bar->HasCapture()) + m_ratio_bar->ReleaseMouse(); + }); + + m_ratio_bar->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_ratio_dragging = false; + }); + + sizer->Add(m_ratio_bar, 0, wxEXPAND); + + auto* pct_sizer = new wxBoxSizer(wxHORIZONTAL); + m_label_ratio_a = new RatioLabelPanel(this); + m_label_ratio_a->SetLabel(wxString::Format(wxT("%d%%"), ratio(0))); + m_label_ratio_b = new RatioLabelPanel(this); + m_label_ratio_b->SetLabel(wxString::Format(wxT("%d%%"), ratio(1))); + auto bind_ratio_click = [this](RatioLabelPanel* label, size_t idx) { + label->Bind(wxEVT_LEFT_DOWN, [this, label, idx](wxMouseEvent&) { + wxRect rect(wxPoint(0, 0), label->GetClientSize()); + start_ratio_editor(idx, label, rect); + }); + }; + bind_ratio_click(m_label_ratio_a, 0); + bind_ratio_click(m_label_ratio_b, 1); + pct_sizer->Add(m_label_ratio_a, 0); + pct_sizer->AddStretchSpacer(1); + pct_sizer->Add(m_label_ratio_b, 0); + sizer->Add(pct_sizer, 0, wxEXPAND | wxTOP, FromDIP(2)); + + return sizer; +} + +// ---- Triangle (ternary) ratio picker ---- + +// Barycentric coordinate utilities +struct TriPoint { double x, y; }; + +static double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c) +{ + return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y); +} + +static bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2) +{ + double total = tri_signed_area2(v0, v1, v2); + if (std::abs(total) < 1e-9) return false; + double s0 = tri_signed_area2(p, v1, v2) / total; + double s1 = tri_signed_area2(v0, p, v2) / total; + double s2 = 1.0 - s0 - s1; + return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001; +} + +static void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2, + double& w0, double& w1, double& w2) +{ + double total = std::abs(tri_signed_area2(v0, v1, v2)); + if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; } + w0 = std::abs(tri_signed_area2(p, v1, v2)) / total; + w1 = std::abs(tri_signed_area2(v0, p, v2)) / total; + w2 = 1.0 - w0 - w1; + w0 = std::clamp(w0, 0.0, 1.0); + w1 = std::clamp(w1, 0.0, 1.0); + w2 = std::clamp(w2, 0.0, 1.0); + double s = w0 + w1 + w2; + if (s > 0) { w0 /= s; w1 /= s; w2 /= s; } +} + +static TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2) +{ + double w0, w1, w2; + tri_barycentric(p, v0, v1, v2, w0, w1, w2); + w0 = std::clamp(w0, 0.0, 1.0); + w1 = std::clamp(w1, 0.0, 1.0); + w2 = std::clamp(w2, 0.0, 1.0); + double s = w0 + w1 + w2; + if (s > 0) { w0 /= s; w1 /= s; w2 /= s; } + return {w0 * v0.x + w1 * v1.x + w2 * v2.x, + w0 * v0.y + w1 * v1.y + w2 * v2.y}; +} + +wxBoxSizer* MixedFilamentDialog::create_triangle_picker() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + int panel_w = FromDIP(160); + int panel_h = FromDIP(160); + m_triangle_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(panel_w, panel_h)); + m_triangle_panel->SetMinSize(wxSize(panel_w, panel_h)); + m_triangle_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_triangle_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + + auto get_vertices = [this]() -> std::tuple { + wxSize sz = m_triangle_panel->GetClientSize(); + double pw = sz.GetWidth(), ph = sz.GetHeight(); + double margin = FromDIP(20); + double avail = std::min(pw, ph) - 2 * margin; + double side = avail; + double tri_h = side * std::sqrt(3.0) / 2.0; + double cx = pw / 2.0; + double top_y = (ph - tri_h) / 2.0; + double bot_y = top_y + tri_h; + TriPoint v0 = {cx, top_y}; // top + TriPoint v1 = {cx - side / 2.0, bot_y}; // bottom-left + TriPoint v2 = {cx + side / 2.0, bot_y}; // bottom-right + return {v0, v1, v2}; + }; + + m_triangle_panel->Bind(wxEVT_PAINT, [this, get_vertices](wxPaintEvent&) { + wxBufferedPaintDC dc(m_triangle_panel); + wxSize sz = m_triangle_panel->GetClientSize(); + auto [v0, v1, v2] = get_vertices(); + + wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); + dc.SetBrush(wxBrush(tri_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2); + + const bool cache_valid = m_tri_cache_bmp.IsOk() && + m_tri_cache_size == sz && + m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2; + + if (!cache_valid) { + int min_y = (int)std::min({v0.y, v1.y, v2.y}); + int max_y = (int)std::max({v0.y, v1.y, v2.y}); + int min_x = (int)std::min({v0.x, v1.x, v2.x}); + int max_x = (int)std::max({v0.x, v1.x, v2.x}); + + m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24); + wxMemoryDC mdc(m_tri_cache_bmp); + mdc.SetBrush(wxBrush(tri_bg)); + mdc.SetPen(*wxTRANSPARENT_PEN); + mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + for (int py = min_y; py <= max_y; ++py) { + for (int px = min_x; px <= max_x; ++px) { + TriPoint p = {(double)px, (double)py}; + if (!tri_contains(p, v0, v1, v2)) continue; + double w0, w1, w2; + tri_barycentric(p, v0, v1, v2, w0, w1, w2); + unsigned char mr, mg, mb; + if (w0 + w1 > 1e-6) { + float t01 = static_cast(w1 / (w0 + w1)); + Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), + c1.Red(), c1.Green(), c1.Blue(), + t01, &mr, &mg, &mb); + float t2 = static_cast(w2); + Slic3r::filament_mixer_lerp(mr, mg, mb, + c2.Red(), c2.Green(), c2.Blue(), + t2, &mr, &mg, &mb); + } else { + mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); + } + mdc.SetPen(wxPen(wxColour(mr, mg, mb))); + mdc.DrawPoint(px, py); + } + } + + mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); + mdc.SetBrush(*wxTRANSPARENT_BRUSH); + wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}}; + mdc.DrawPolygon(3, pts); + + mdc.SelectObject(wxNullBitmap); + m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2; + m_tri_cache_size = sz; + } + + dc.DrawBitmap(m_tri_cache_bmp, 0, 0); + + // Drag handle (always redrawn on top of cached bitmap) + double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x; + double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y; + int handle_r = FromDIP(5); + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2))); + dc.DrawCircle((int)hx, (int)hy, handle_r); + + if (m_result.ratios.size() >= 3) { + dc.SetFont(::Label::Body_10); + wxSize ts0 = dc.GetTextExtent(wxString::Format(wxT("%d%%"), m_result.ratios[0])); + int top_label_y = std::max(0, (int)(v0.y - ts0.GetHeight() - FromDIP(4))); + + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + dc.DrawText(_L("Ratio"), FromDIP(2), top_label_y); + + // Position the real RatioLabelPanel children + for (int i = 0; i < 3 && i < (int)m_triangle_ratio_labels.size(); ++i) { + if (!m_triangle_ratio_labels[i]) continue; + m_triangle_ratio_labels[i]->SetLabel( + wxString::Format(wxT("%d%%"), m_result.ratios[i])); + wxSize lsz = m_triangle_ratio_labels[i]->GetMinSize(); + int lx = 0, ly = 0; + if (i == 0) { + lx = (int)(v0.x - lsz.GetWidth() / 2); + ly = top_label_y; + } else if (i == 1) { + lx = (int)(v1.x - lsz.GetWidth() / 2); + ly = (int)(v1.y + FromDIP(3)); + } else { + lx = (int)(v2.x - lsz.GetWidth() / 2); + ly = (int)(v2.y + FromDIP(3)); + } + m_triangle_ratio_labels[i]->SetSize(lx, ly, lsz.GetWidth(), lsz.GetHeight()); + } + } + }); + + auto handle_mouse = [this, get_vertices](wxMouseEvent& e, bool is_down) { + auto [v0, v1, v2] = get_vertices(); + TriPoint p = {(double)e.GetX(), (double)e.GetY()}; + + if (is_down) { + // Only start dragging when the press lands inside the triangle; + // clicks outside the triangle must not change the mix ratio. + if (!tri_contains(p, v0, v1, v2)) + return; + m_tri_dragging = true; + if (!m_triangle_panel->HasCapture()) + m_triangle_panel->CaptureMouse(); + } + + if (!m_tri_dragging) return; + + TriPoint clamped = tri_clamp(p, v0, v1, v2); + tri_barycentric(clamped, v0, v1, v2, m_tri_wx, m_tri_wy, m_tri_wz); + + int r0 = (int)(m_tri_wx * 100 + 0.5); + int r1 = (int)(m_tri_wy * 100 + 0.5); + int r2 = 100 - r0 - r1; + r0 = std::clamp(r0, 0, 100); + r1 = std::clamp(r1, 0, 100); + r2 = std::clamp(r2, 0, 100); + + apply_dragged_triangle_ratio(r0, r1, r2); + }; + + // Create 3 RatioLabelPanel children on the triangle panel + m_triangle_ratio_labels.fill(nullptr); + for (int i = 0; i < 3; ++i) { + auto* lbl = new RatioLabelPanel(m_triangle_panel); + lbl->SetLabel(wxString::Format(wxT("%d%%"), + (i < (int)m_result.ratios.size()) ? m_result.ratios[i] : 33)); + size_t idx = (size_t)i; + lbl->Bind(wxEVT_LEFT_DOWN, [this, lbl, idx](wxMouseEvent&) { + wxRect rect(wxPoint(0, 0), lbl->GetClientSize()); + start_ratio_editor(idx, lbl, rect); + }); + m_triangle_ratio_labels[i] = lbl; + } + + m_triangle_panel->Bind(wxEVT_LEFT_DOWN, [this, handle_mouse](wxMouseEvent& e) { + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) + commit_ratio_editor(true); + handle_mouse(e, true); + }); + m_triangle_panel->Bind(wxEVT_MOTION, [this, handle_mouse](wxMouseEvent& e) { + if (m_tri_dragging) + handle_mouse(e, false); + }); + m_triangle_panel->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { + m_tri_dragging = false; + if (m_triangle_panel->HasCapture()) + m_triangle_panel->ReleaseMouse(); + }); + m_triangle_panel->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_tri_dragging = false; + }); + + sizer->Add(m_triangle_panel, 0); + + return sizer; +} + +wxBoxSizer* MixedFilamentDialog::create_gradient_section() +{ + m_gradient_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_chk_gradient = new ::CheckBox(this); + m_chk_gradient->SetValue(m_result.gradient_enabled); + m_chk_gradient->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); on_gradient_toggled(); }); + m_gradient_sizer->Add(m_chk_gradient, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, FromDIP(4)); + + m_label_gradient = new wxStaticText(this, wxID_ANY, _L("Gradient Effect")); + m_label_gradient->SetFont(::Label::Body_13); + m_gradient_sizer->Add(m_label_gradient, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + m_combo_gradient_dir = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(152), FromDIP(24)), 0, nullptr, wxCB_READONLY); + m_combo_gradient_dir->SetKeepDropArrow(true); + update_gradient_direction_items(); + m_combo_gradient_dir->SetSelection(m_result.gradient_direction); + m_combo_gradient_dir->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_gradient_direction_changed(); }); + m_combo_gradient_dir->Show(m_result.gradient_enabled); + + m_gradient_sizer->Add(m_combo_gradient_dir, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + auto* outer = new wxBoxSizer(wxVERTICAL); + outer->Add(m_gradient_sizer, 0, wxEXPAND); + + // Custom curve editor: visible only when gradient is on and exactly 2 components are mixed. + m_curve_sizer = new wxBoxSizer(wxVERTICAL); + m_curve_editor = new GradientCurveEditor(this, comp_colour(0), comp_colour(1)); + if (!m_result.gradient_curve.empty()) + m_curve_editor->set_points(m_result.gradient_curve); + else + m_curve_editor->reset_to_linear((m_result.gradient_direction == 0) ? 0.9 : 0.1, + (m_result.gradient_direction == 0) ? 0.1 : 0.9); + m_curve_editor->Bind(wxEVT_GRADIENT_CURVE_CHANGED, + [this](wxCommandEvent&) { on_gradient_curve_changed(); }); + m_curve_sizer->Add(m_curve_editor, 0, wxEXPAND | wxTOP, FromDIP(4)); + + outer->Add(m_curve_sizer, 0, wxEXPAND | wxTOP, FromDIP(6)); + const bool curve_visible = m_result.gradient_enabled && num_components() == 2; + m_curve_sizer->ShowItems(curve_visible); + + // Per-part gradient toggle sits BELOW the curve editor. + m_per_part_gradient_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_chk_per_part_gradient = new ::CheckBox(this); + m_chk_per_part_gradient->SetValue(m_result.per_part_gradient); + m_chk_per_part_gradient->Bind(wxEVT_TOGGLEBUTTON, + [this](wxCommandEvent& e) { e.Skip(); on_per_part_gradient_toggled(); }); + m_per_part_gradient_sizer->Add(m_chk_per_part_gradient, 0, + wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, FromDIP(4)); + + m_label_per_part_gradient = new wxStaticText(this, wxID_ANY, _L("Enable per-part gradient effect")); + m_label_per_part_gradient->SetFont(::Label::Body_13); + m_per_part_gradient_sizer->Add(m_label_per_part_gradient, 0, + wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + outer->Add(m_per_part_gradient_sizer, 0, wxEXPAND | wxTOP, FromDIP(2)); + m_per_part_gradient_sizer->ShowItems(m_result.gradient_enabled); + + return outer; +} + +wxBoxSizer* MixedFilamentDialog::create_recommendation_grid() +{ + auto* outer = new wxBoxSizer(wxVERTICAL); + + auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* rec_label = new wxStaticText(this, wxID_ANY, _L("Mixing Recommendations")); + rec_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#ACACAC"))); + rec_label->SetFont(::Label::Body_10); + title_sizer->Add(rec_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + + auto* rec_line = new wxPanel(this, wxID_ANY); + rec_line->SetMinSize(wxSize(-1, 1)); + rec_line->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#EEEEEE"))); + title_sizer->Add(rec_line, 1, wxALIGN_CENTER_VERTICAL); + + outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116))); + m_recommendation_scroll->SetScrollRate(0, 5); + m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); + + m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES); + auto* scroll_inner_sizer = new wxBoxSizer(wxVERTICAL); + scroll_inner_sizer->Add(m_recommendation_grid, 1, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + m_recommendation_scroll->SetSizer(scroll_inner_sizer); + + rebuild_recommendation_items(); + + outer->Add(m_recommendation_scroll, 1, wxEXPAND | wxTOP, FromDIP(4)); + return outer; +} + +void MixedFilamentDialog::rebuild_recommendation_items() +{ + if (!m_recommendation_scroll || !m_recommendation_grid) + return; + + static constexpr int MAX_RECOMMENDATIONS = 100; + + m_recommendation_scroll->Freeze(); + m_recommendation_grid->Clear(true); + + size_t n = m_physical_colors.size(); + int count = 0; + + // Group physical filaments by type (only same-type combos are recommended) + std::map> type_groups; + for (size_t i = 0; i < n; ++i) { + std::string t = (i < m_physical_types.size()) ? m_physical_types[i] : "PLA"; + // Skip support filaments (type ends with "-S") + if (t.size() >= 2 && t.compare(t.size() - 2, 2, "-S") == 0) + continue; + type_groups[t].push_back(i); + } + + if (num_components() >= 3) { + // Three-color: C(g,3) x 3 variants per same-type group + for (auto& [type, indices] : type_groups) { + if (count >= MAX_RECOMMENDATIONS) break; + size_t g = indices.size(); + for (size_t ai = 0; ai < g && count < MAX_RECOMMENDATIONS; ++ai) { + for (size_t bi = ai + 1; bi < g && count < MAX_RECOMMENDATIONS; ++bi) { + for (size_t ci = bi + 1; ci < g && count < MAX_RECOMMENDATIONS; ++ci) { + size_t idx[3] = {indices[ai], indices[bi], indices[ci]}; + // 3 variants: each filament takes the 50% role in turn + for (int dominant = 0; dominant < 3 && count < MAX_RECOMMENDATIONS; ++dominant) { + size_t i0 = idx[(dominant + 1) % 3]; // 25% + size_t i1 = idx[(dominant + 2) % 3]; // 25% + size_t i2 = idx[dominant]; // 50% + + wxColour ca(m_physical_colors[i0]); + wxColour cb(m_physical_colors[i1]); + wxColour cc(m_physical_colors[i2]); + wxColour mixed = blend_n_colors({ca, cb, cc}, {0.25, 0.25, 0.50}); + + auto* item = new wxPanel(m_recommendation_scroll, wxID_ANY, + wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20))); + item->SetBackgroundColour(mixed); + item->SetCursor(wxCursor(wxCURSOR_HAND)); + + unsigned int ca_1 = (unsigned int)(i0 + 1); + unsigned int cb_1 = (unsigned int)(i1 + 1); + unsigned int cc_1 = (unsigned int)(i2 + 1); + item->Bind(wxEVT_LEFT_UP, [this, ca_1, cb_1, cc_1](wxMouseEvent&) { + on_recommendation_clicked_triple(ca_1, cb_1, cc_1); + }); + item->SetToolTip(wxString::Format(wxT("%s + %s + %s"), + wxString::FromUTF8(m_physical_names[i0]), + wxString::FromUTF8(m_physical_names[i1]), + wxString::FromUTF8(m_physical_names[i2]))); + + m_recommendation_grid->Add(item, 0, wxRIGHT | wxBOTTOM, FromDIP(6)); + ++count; + } + } + } + } + } + } else { + // Two-color: C(g,2) per same-type group + for (auto& [type, indices] : type_groups) { + if (count >= MAX_RECOMMENDATIONS) break; + size_t g = indices.size(); + for (size_t ai = 0; ai < g && count < MAX_RECOMMENDATIONS; ++ai) { + for (size_t bi = ai + 1; bi < g && count < MAX_RECOMMENDATIONS; ++bi) { + size_t i = indices[ai]; + size_t j = indices[bi]; + + wxColour ca(m_physical_colors[i]); + wxColour cb(m_physical_colors[j]); + wxColour mixed = blend_colors(ca, cb, 0.5); + + auto* item = new wxPanel(m_recommendation_scroll, wxID_ANY, + wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20))); + item->SetBackgroundColour(mixed); + item->SetCursor(wxCursor(wxCURSOR_HAND)); + + unsigned int comp_a = (unsigned int)(i + 1); + unsigned int comp_b = (unsigned int)(j + 1); + item->Bind(wxEVT_LEFT_UP, [this, comp_a, comp_b](wxMouseEvent&) { + on_recommendation_clicked(comp_a, comp_b); + }); + item->SetToolTip(wxString::Format(wxT("%s + %s"), + wxString::FromUTF8(m_physical_names[i]), + wxString::FromUTF8(m_physical_names[j]))); + + m_recommendation_grid->Add(item, 0, wxRIGHT | wxBOTTOM, FromDIP(6)); + ++count; + } + } + } + } + + m_recommendation_scroll->SetScrollbars(0, FromDIP(20), 0, 1); + m_recommendation_scroll->FitInside(); + m_recommendation_scroll->Layout(); + m_recommendation_scroll->Thaw(); +} + +wxBoxSizer* MixedFilamentDialog::create_button_panel() +{ + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + + 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 MixedFilamentDialog::rebuild_all_combos() +{ + m_combo_to_physical.resize(m_combo_filaments.size()); + + for (size_t i = 0; i < m_combo_filaments.size(); ++i) { + std::set others_selected; + std::set others_types; + for (size_t k = 0; k < m_result.components.size(); ++k) { + if (k == i) continue; + unsigned int phys = m_result.components[k]; + others_selected.insert(phys); + if (phys >= 1 && phys <= m_physical_types.size()) + others_types.insert(m_physical_types[phys - 1]); + } + + auto* combo = m_combo_filaments[i]; + combo->Clear(); + m_combo_to_physical[i].clear(); + + int restore_sel = -1; + unsigned int cur_phys = (i < m_result.components.size()) ? m_result.components[i] : 0; + + if (cur_phys == 0) { + combo->Append(_L("-- Select --")); + m_combo_to_physical[i].push_back(0); + restore_sel = 0; + } + + for (size_t j = 0; j < m_physical_names.size(); ++j) { + unsigned int phys_1based = (unsigned int)(j + 1); + + if (others_selected.count(phys_1based)) + continue; + + int style = 0; + if (!others_types.empty() && !m_physical_types.empty()) { + std::string this_type = (j < m_physical_types.size()) ? m_physical_types[j] : "PLA"; + if (others_types.find(this_type) == others_types.end()) + style = DD_ITEM_STYLE_DIMMED; + } + + int idx = combo->Append(wxString::FromUTF8(m_physical_names[j]), make_swatch_bitmap(j), style); + m_combo_to_physical[i].push_back(phys_1based); + + if (phys_1based == cur_phys) + restore_sel = idx; + } + + if (restore_sel >= 0) + combo->SetSelection(restore_sel); + else if (combo->GetCount() > 0) + combo->SetSelection(0); + } +} + +void MixedFilamentDialog::refresh_curve_editor_colors() +{ + if (m_curve_editor) + m_curve_editor->set_colors(comp_colour(0), comp_colour(1)); +} + +// ---- Event Handlers ---- + +void MixedFilamentDialog::on_filament_changed() +{ + for (size_t i = 0; i < m_combo_filaments.size() && i < m_result.components.size(); ++i) { + int sel = m_combo_filaments[i]->GetSelection(); + if (sel >= 0 && i < m_combo_to_physical.size() && sel < (int)m_combo_to_physical[i].size()) + m_result.components[i] = m_combo_to_physical[i][sel]; + } + + refresh_curve_editor_colors(); + rebuild_all_combos(); + update_gradient_direction_items(); + update_preview(); + update_ok_button_state(); +} + +void MixedFilamentDialog::on_ratio_changed(int new_ratio_a) +{ + if (m_result.ratios.size() < 2) return; + m_result.ratios[0] = new_ratio_a; + m_result.ratios[1] = 100 - new_ratio_a; + + reset_manual_ratio_state(); + refresh_ratio_labels(); + + update_preview(); +} + +void MixedFilamentDialog::on_gradient_toggled() +{ + + m_result.gradient_enabled = m_chk_gradient->GetValue(); + + if (m_ratio_sizer) + m_ratio_sizer->ShowItems(!m_result.gradient_enabled && num_components() == 2); + if (m_combo_gradient_dir) + m_combo_gradient_dir->Show(m_result.gradient_enabled); + if (m_per_part_gradient_sizer) + m_per_part_gradient_sizer->ShowItems(m_result.gradient_enabled); + if (m_curve_sizer) + m_curve_sizer->ShowItems(m_result.gradient_enabled && num_components() == 2); + if (!m_result.gradient_enabled) { + m_result.per_part_gradient = false; + if (m_chk_per_part_gradient) m_chk_per_part_gradient->SetValue(false); + } + + // Toggling the curve editor changes the right column height (and width when + // turning gradient on), so the dialog must follow or the recommendation list + // gets squeezed off-screen. Same trick as 2-color -> 3-color switching. + const wxSize new_size = compute_dialog_size(); + if (GetSize() != new_size) { + const wxRect old_rect = GetRect(); + const wxPoint center(old_rect.x + old_rect.width / 2, + old_rect.y + old_rect.height / 2); + SetSize(new_size); + SetPosition(wxPoint(center.x - new_size.x / 2, + center.y - new_size.y / 2)); + } + + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_gradient_direction_changed() +{ + if (!m_combo_gradient_dir) return; + m_result.gradient_direction = m_combo_gradient_dir->GetSelection(); + + // Mirror the user's custom curve around y=0.5 instead of resetting it, so + // shape work (added anchors, bent segments) survives a direction toggle. + // reverse() flips y and tangent signs consistently; default two-point + // linear curves end up matching the new direction exactly (0.9->0.1 <-> 0.1->0.9). + if (m_curve_editor) { + m_curve_editor->reverse(); + m_result.gradient_curve = m_curve_editor->get_points(); + } + update_preview(); +} + +void MixedFilamentDialog::on_gradient_curve_changed() +{ + if (m_curve_editor) + m_result.gradient_curve = m_curve_editor->get_points(); + update_preview(); +} + +void MixedFilamentDialog::on_per_part_gradient_toggled() +{ + if (m_chk_per_part_gradient) + m_result.per_part_gradient = m_chk_per_part_gradient->GetValue(); +} + +void MixedFilamentDialog::on_add_material() +{ + size_t n = num_components(); + if (n >= (size_t)MAX_COMPONENTS) return; + + unsigned int new_comp = 0; + for (size_t j = 0; j < m_physical_names.size(); ++j) { + unsigned int candidate = (unsigned int)(j + 1); + bool used = false; + for (auto c : m_result.components) + if (c == candidate) { used = true; break; } + if (!used) { new_comp = candidate; break; } + } + if (new_comp == 0) return; + m_result.components.push_back(new_comp); + + int each = 100 / (int)(n + 1); + m_result.ratios.clear(); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + m_result.ratios.push_back(each); + assigned += each; + } + m_result.ratios.push_back(100 - assigned); + + if (m_result.ratios.size() >= 3) { + int sum = 0; + for (int r : m_result.ratios) sum += r; + if (sum > 0) { + m_tri_wx = (double)m_result.ratios[0] / sum; + m_tri_wy = (double)m_result.ratios[1] / sum; + m_tri_wz = (double)m_result.ratios[2] / sum; + } + } + reset_manual_ratio_state(); + + append_material_row(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + rebuild_recommendation_items(); + + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_remove_material() +{ + if (num_components() <= 2) + return; + + m_result.components.resize(2); + m_result.ratios = {50, 50}; + m_tri_wx = 0.5; + m_tri_wy = 0.5; + m_tri_wz = 0.0; + + reset_manual_ratio_state(); + refresh_ratio_labels(); + + if (m_material_rows_sizer && m_material_rows_sizer->GetItemCount() > 2) { + auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); + if (sizer_item && sizer_item->GetSizer()) + sizer_item->GetSizer()->Clear(true); + m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); + } + + if (m_combo_filaments.size() > 2) + m_combo_filaments.pop_back(); + if (m_combo_to_physical.size() > 2) + m_combo_to_physical.pop_back(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + rebuild_recommendation_items(); + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b) +{ + while (m_material_rows_sizer->GetItemCount() > 2) { + auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); + if (sizer_item && sizer_item->GetSizer()) + sizer_item->GetSizer()->Clear(true); + m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); + } + + while (m_combo_filaments.size() > 2) + m_combo_filaments.pop_back(); + while (m_combo_to_physical.size() > 2) + m_combo_to_physical.pop_back(); + + m_result.components = {comp_a, comp_b}; + m_result.ratios = {50, 50}; + + reset_manual_ratio_state(); + refresh_ratio_labels(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_gradient_direction_items(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c) +{ + // Ensure we have exactly 3 combo rows + if (num_components() < 3) { + // Need to add a 3rd combo row + while (m_combo_filaments.size() < 3) + append_material_row(); + } else if (num_components() > 3) { + while (m_material_rows_sizer->GetItemCount() > 3) { + auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); + if (sizer_item && sizer_item->GetSizer()) + sizer_item->GetSizer()->Clear(true); + m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); + } + while (m_combo_filaments.size() > 3) + m_combo_filaments.pop_back(); + while (m_combo_to_physical.size() > 3) + m_combo_to_physical.pop_back(); + } + + m_result.components = {a, b, c}; + m_result.ratios = {25, 25, 50}; + m_tri_wx = 0.25; + m_tri_wy = 0.25; + m_tri_wz = 0.50; + reset_manual_ratio_state(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_gradient_direction_items(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + Layout(); + Refresh(); +} + +void MixedFilamentDialog::update_preview() +{ + if (m_preview_canvas) m_preview_canvas->Refresh(); + if (m_summary_panel) m_summary_panel->Refresh(); + if (m_ratio_bar) m_ratio_bar->Refresh(); + if (m_triangle_panel) m_triangle_panel->Refresh(); +} + +void MixedFilamentDialog::paint_warning_panel(wxPaintEvent&) +{ + wxBufferedPaintDC dc(m_warning_panel); + wxSize sz = m_warning_panel->GetClientSize(); + + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#F8F8F8")))); + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#D01B1B")), 1)); + dc.DrawRoundedRectangle(0, 0, sz.GetWidth(), sz.GetHeight(), FromDIP(4)); + + int x = FromDIP(10); + int cy = sz.GetHeight() / 2; + + int icon_r = FromDIP(7); + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#D01B1B")))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawCircle(x + icon_r, cy, icon_r); + dc.SetFont(::Label::Body_10); + dc.SetTextForeground(*wxWHITE); + wxSize ex = dc.GetTextExtent(wxT("!")); + dc.DrawText(wxT("!"), x + icon_r - ex.GetWidth() / 2, cy - ex.GetHeight() / 2); + x += icon_r * 2 + FromDIP(6); + + if (m_type_mismatch_msg.empty()) return; + + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#D01B1B"))); + wxString msg = m_type_mismatch_msg; + int avail_w = sz.GetWidth() - x - FromDIP(10); + wxSize ts = dc.GetTextExtent(msg); + if (ts.GetWidth() <= avail_w) { + dc.DrawText(msg, x, cy - ts.GetHeight() / 2); + } else { + wxArrayString lines; + wxString cur_line; + wxArrayString words; + wxStringTokenizer tkz(msg, wxT(" "), wxTOKEN_RET_EMPTY_ALL); + while (tkz.HasMoreTokens()) words.Add(tkz.GetNextToken()); + if (words.empty()) words.Add(msg); + for (size_t w = 0; w < words.size(); ++w) { + wxString test = cur_line.empty() ? words[w] : cur_line + wxT(" ") + words[w]; + if (dc.GetTextExtent(test).GetWidth() > avail_w && !cur_line.empty()) { + lines.Add(cur_line); + cur_line = words[w]; + } else { + cur_line = test; + } + } + if (!cur_line.empty()) lines.Add(cur_line); + if (lines.empty()) lines.Add(msg); + int line_h = dc.GetTextExtent(wxT("Mg")).GetHeight(); + int total_h = (int)lines.size() * line_h; + int y0 = (sz.GetHeight() - total_h) / 2; + for (size_t l = 0; l < lines.size(); ++l) + dc.DrawText(lines[l], x, y0 + (int)l * line_h); + } +} + +void MixedFilamentDialog::update_ok_button_state() +{ + if (!m_btn_ok) return; + + bool has_type_mismatch = false; + if (!m_physical_types.empty() && m_result.components.size() >= 2) { + std::map> type_groups; + for (size_t i = 0; i < m_result.components.size(); ++i) { + unsigned int phys = m_result.components[i]; + if (phys < 1 || phys > m_physical_types.size()) continue; + type_groups[m_physical_types[phys - 1]].push_back(phys); + } + has_type_mismatch = type_groups.size() > 1; + if (has_type_mismatch) { + wxString parts; + for (auto it = type_groups.begin(); it != type_groups.end(); ++it) { + if (!parts.empty()) + parts += _L(" and "); + wxString slots; + for (size_t j = 0; j < it->second.size(); ++j) { + if (!slots.empty()) slots += ", "; + slots += std::to_string(it->second[j]); + } + parts += wxString::Format(_L("Slot %s (%s)"), slots, wxString::FromUTF8(it->first)); + } + m_type_mismatch_msg = parts + " " + _L("cannot be mixed. Please select the same filament type."); + } else { + m_type_mismatch_msg.clear(); + } + } else { + m_type_mismatch_msg.clear(); + } + + bool has_unselected = false; + for (unsigned int c : m_result.components) { + if (c == 0) { has_unselected = true; break; } + } + + bool can_confirm = !has_type_mismatch && !has_unselected; + // Enable() alone repaints the button: its StateColor carries the disabled grey. + m_btn_ok->Enable(can_confirm); + if (has_unselected) + m_btn_ok->SetToolTip(_L("Please select a filament for all components")); + else if (has_type_mismatch) + m_btn_ok->SetToolTip(_L("Cannot mix different filament types")); + else + m_btn_ok->SetToolTip(wxEmptyString); + + if (m_warning_panel) { + m_warning_panel->Show(has_type_mismatch); + // Force a repaint: when the panel is already visible and only the + // mismatch text changes (e.g. PETG -> ABS), Show()/Layout() do not + // generate a paint event, so paint_warning_panel keeps the stale text. + m_warning_panel->Refresh(); + Layout(); + } +} + +void MixedFilamentDialog::update_gradient_direction_items() +{ + if (!m_combo_gradient_dir) return; + + int prev_sel = m_combo_gradient_dir->GetSelection(); + m_combo_gradient_dir->Clear(); + + if (num_components() < 2) return; + + auto make_direction_bitmap = [this](size_t idx_from, size_t idx_to) -> wxBitmap { + int swatch_sz = FromDIP(20); + int arrow_w = FromDIP(16); + int gap = FromDIP(4); + int bmp_w = swatch_sz + gap + arrow_w + gap + swatch_sz; + int bmp_h = swatch_sz; + + wxColour dir_text = StateColor::darkModeColorFor(wxColour("#262E30")); + + return make_alpha_bitmap(bmp_w, bmp_h, [&](wxDC& dc) { + dc.SetFont(::Label::Body_13); + + auto draw_swatch = [&](int x, size_t idx) { + std::string color_hex = "#D9D9D9"; + if (idx < m_physical_colors.size()) + color_hex = m_physical_colors[idx]; + std::string label = std::to_string(idx + 1); + wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); + if (icon && icon->IsOk()) + dc.DrawBitmap(*icon, x, 0); + }; + + int x = 0; + draw_swatch(x, idx_from); + x += swatch_sz + gap; + + dc.SetTextForeground(dir_text); + wxString arrow = wxT("\u2192"); + wxSize arrow_sz = dc.GetTextExtent(arrow); + dc.DrawText(arrow, x + (arrow_w - arrow_sz.GetWidth()) / 2, + (bmp_h - arrow_sz.GetHeight()) / 2); + x += arrow_w + gap; + + draw_swatch(x, idx_to); + }); + }; + + size_t idx_a = (comp(0) >= 1) ? comp(0) - 1 : 0; + size_t idx_b = (comp(1) >= 1) ? comp(1) - 1 : 1; + + m_combo_gradient_dir->Append(wxT(" "), make_direction_bitmap(idx_a, idx_b)); + m_combo_gradient_dir->Append(wxT(" "), make_direction_bitmap(idx_b, idx_a)); + + if (prev_sel >= 0 && prev_sel < (int)m_combo_gradient_dir->GetCount()) + m_combo_gradient_dir->SetSelection(prev_sel); + else + m_combo_gradient_dir->SetSelection(0); +} + +wxSize MixedFilamentDialog::compute_dialog_size() const +{ + const bool is_three = (num_components() >= 3); + const bool curve_visible = !is_three && m_result.gradient_enabled; + + int w = FromDIP(439); + int h = FromDIP(580); + if (is_three) { + h = FromDIP(680); + } else if (curve_visible) { + // Wider so the gradient editor can show "Material Ratio" intact; + // +40 over the 3-color height to fit the curve editor while keeping the + // recommendation list visible (it can still scroll if needed). + w = FromDIP(470); + h = FromDIP(720); + } + return wxSize(w, h); +} + +void MixedFilamentDialog::update_component_count_ui() +{ + bool is_two = (num_components() == 2); + bool is_three = (num_components() >= 3); + + // Toggle ratio slider vs triangle picker + if (m_ratio_sizer) + m_ratio_sizer->ShowItems(is_two && !m_result.gradient_enabled); + if (m_triangle_sizer) + m_triangle_sizer->ShowItems(is_three); + + // 3-color: hide gradient entirely, force off + if (m_gradient_sizer) { + bool show_gradient = is_two; + m_chk_gradient->Show(show_gradient); + if (m_label_gradient) m_label_gradient->Show(show_gradient); + m_combo_gradient_dir->Show(show_gradient && m_result.gradient_enabled); + if (m_per_part_gradient_sizer) + m_per_part_gradient_sizer->ShowItems(show_gradient && m_result.gradient_enabled); + if (m_curve_sizer) + m_curve_sizer->ShowItems(show_gradient && m_result.gradient_enabled); + } + if (is_three) { + m_result.gradient_enabled = false; + if (m_chk_gradient) m_chk_gradient->SetValue(false); + m_result.per_part_gradient = false; + if (m_chk_per_part_gradient) m_chk_per_part_gradient->SetValue(false); + } + + if (m_btn_add_material) { + bool can_add = (num_components() < (size_t)MAX_COMPONENTS && m_physical_colors.size() > num_components()); + m_btn_add_material->Enable(can_add); + m_btn_add_material->SetToolTip(can_add ? wxString() + : (is_three ? _L("Maximum 3 materials for mixing") : _L("Maximum number of components reached"))); + } + + if (m_btn_remove_material) { + m_btn_remove_material->Show(is_three); + m_btn_remove_material->Enable(is_three); + m_btn_remove_material->SetToolTip(is_three ? _L("Remove the third material") : wxString()); + } + + const wxSize new_size = compute_dialog_size(); + const wxRect old_rect = GetRect(); + const wxPoint center(old_rect.x + old_rect.width / 2, + old_rect.y + old_rect.height / 2); + SetSize(new_size); + SetPosition(wxPoint(center.x - new_size.x / 2, + center.y - new_size.y / 2)); + Layout(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/MixedFilamentDialog.hpp b/src/slic3r/GUI/MixedFilamentDialog.hpp new file mode 100644 index 0000000000..ea8ac5ad16 --- /dev/null +++ b/src/slic3r/GUI/MixedFilamentDialog.hpp @@ -0,0 +1,183 @@ +#ifndef slic3r_MixedFilamentDialog_hpp_ +#define slic3r_MixedFilamentDialog_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GUI_Utils.hpp" +#include "libslic3r/FilamentMixer.hpp" + +class Button; +class CheckBox; +class ComboBox; +class wxMouseEvent; +class wxScrolledWindow; +class wxTextCtrl; +class wxWrapSizer; + +namespace Slic3r { +namespace GUI { + +class GradientCurveEditor; +class RatioLabelPanel; + +struct MixedFilamentResult { + std::vector components; // 1-based physical filament indices + std::vector ratios; // percentages, sum = 100 + bool gradient_enabled = false; + int gradient_direction = 0; // 0 = A→B, 1 = B→A (only for 2-color) + bool per_part_gradient = false; // valid only when gradient_enabled == true + // Optional Photoshop-style custom curve overriding the linear A→B gradient. + // Empty -> use linear (gradient_direction). Non-empty -> cubic Hermite over [0,1]^2 + // with optional per-anchor tangent overrides (see GradientAnchor). + std::vector gradient_curve; +}; + +class MixedFilamentDialog : public DPIDialog +{ +public: + MixedFilamentDialog(wxWindow* parent, + const std::vector& physical_colors, + const std::vector& physical_names, + const std::vector& physical_types = {}); + + MixedFilamentDialog(wxWindow* parent, + const MixedFilamentResult& existing, + const std::vector& physical_colors, + const std::vector& physical_names, + const std::vector& physical_types = {}); + + ~MixedFilamentDialog(); + + MixedFilamentResult get_result() const { return m_result; } + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + +private: + void build_ui(); + wxBoxSizer* create_preview_panel(); + wxBoxSizer* create_material_selection(); + wxBoxSizer* create_ratio_slider(); + wxBoxSizer* create_triangle_picker(); + wxBoxSizer* create_gradient_section(); + wxBoxSizer* create_recommendation_grid(); + wxBoxSizer* create_button_panel(); + + void on_filament_changed(); + void on_ratio_changed(int new_ratio_a); + void on_gradient_toggled(); + void on_gradient_direction_changed(); + void on_gradient_curve_changed(); + void on_per_part_gradient_toggled(); + void on_add_material(); + void on_remove_material(); + void on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b); + void on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c); + void apply_manual_ratio(size_t idx, int value); + void apply_dragged_triangle_ratio(int r0, int r1, int r2); + void reset_manual_ratio_state(); + void refresh_ratio_labels(); + void sync_triangle_weights_from_ratios(); + void start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect); + void commit_ratio_editor(bool apply); + void commit_ratio_editor_from_background(wxMouseEvent& e); + void update_preview(); + void update_ok_button_state(); + void update_gradient_direction_items(); + void update_component_count_ui(); + // Picks dialog (width, height) based on current state so the gradient curve + // editor and the recommendation list stay visible at the same time. + wxSize compute_dialog_size() const; + void rebuild_all_combos(); + void rebuild_recommendation_items(); + void refresh_curve_editor_colors(); + void paint_warning_panel(wxPaintEvent& evt); + + wxBitmap make_swatch_bitmap(size_t idx); + + // Reserves the same width on every material row label so the combo boxes line up. + static void apply_uniform_label_width(wxStaticText* lbl); + // Appends one "Filament N" label + combo row to m_material_rows_sizer. N follows the + // number of rows already there, so callers must not renumber anything themselves. + void append_material_row(); + + // Helpers for component/ratio access + size_t num_components() const { return m_result.components.size(); } + unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; } + int ratio(size_t i) const { return (i < m_result.ratios.size()) ? m_result.ratios[i] : 0; } + wxColour comp_colour(size_t i) const; + + MixedFilamentResult m_result; + bool m_edit_mode{false}; + std::vector m_physical_colors; + std::vector m_physical_names; + std::vector m_physical_types; + wxString m_type_mismatch_msg; + + // Combo item index -> 1-based physical filament index (per combo) + std::vector> m_combo_to_physical; + + // UI controls + wxPanel* m_preview_canvas{nullptr}; + wxPanel* m_summary_panel{nullptr}; + std::vector m_combo_filaments; + wxBoxSizer* m_material_rows_sizer{nullptr}; + wxPanel* m_ratio_bar{nullptr}; + wxPanel* m_triangle_panel{nullptr}; + RatioLabelPanel* m_label_ratio_a{nullptr}; + RatioLabelPanel* m_label_ratio_b{nullptr}; + wxPanel* m_ratio_editor_panel{nullptr}; + wxTextCtrl* m_ratio_editor{nullptr}; + CheckBox* m_chk_gradient{nullptr}; + wxStaticText* m_label_gradient{nullptr}; + ComboBox* m_combo_gradient_dir{nullptr}; + wxBoxSizer* m_gradient_sizer{nullptr}; + GradientCurveEditor* m_curve_editor{nullptr}; + wxBoxSizer* m_curve_sizer{nullptr}; + CheckBox* m_chk_per_part_gradient{nullptr}; + wxStaticText* m_label_per_part_gradient{nullptr}; + wxBoxSizer* m_per_part_gradient_sizer{nullptr}; + Button* m_btn_add_material{nullptr}; + Button* m_btn_remove_material{nullptr}; + Button* m_btn_ok{nullptr}; + Button* m_btn_cancel{nullptr}; + wxBoxSizer* m_warning_sizer{nullptr}; + wxPanel* m_warning_panel{nullptr}; + + wxBoxSizer* m_ratio_sizer{nullptr}; + wxBoxSizer* m_triangle_sizer{nullptr}; + wxBoxSizer* m_right_sizer{nullptr}; + + wxScrolledWindow* m_recommendation_scroll{nullptr}; + wxWrapSizer* m_recommendation_grid{nullptr}; + + // Drag state. The ratio bar and the triangle picker capture the mouse + // independently, so they must not share a flag: a mouse-up on one would + // otherwise clear the other's flag and skip its ReleaseMouse(). + bool m_ratio_dragging{false}; + bool m_tri_dragging{false}; + std::vector m_ratio_manual_order; + size_t m_ratio_editor_idx{0}; + bool m_ratio_editor_committing{false}; + wxWindow* m_ratio_editor_anchor{nullptr}; + // Triangle picker drag point (barycentric weights) + double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334}; + + // Cached triangle color bitmap (invalidated when colors or size change) + wxBitmap m_tri_cache_bmp; + wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2; + wxSize m_tri_cache_size; + std::array m_triangle_ratio_labels{nullptr, nullptr, nullptr}; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_MixedFilamentDialog_hpp_ diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 22720bf33e..bf0a5cfe1a 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -162,6 +162,8 @@ enum class NotificationType //BBL: plugin install hint BBLPluginInstallHint, BBLFlushingVolumeZero, + // A mixed-color filament references a deleted component, or its components disagree in type. + BBLMixedFilamentBroken, BBLPluginUpdateAvailable, BBLPreviewOnlyMode, BBLPrinterConfigUpdateAvailable, @@ -172,6 +174,8 @@ enum class NotificationType BBLBedFilamentIncompatible, BBLMixUsePLAAndPETG, BBLNozzleFilamentIncompatible, + // A mixed-color filament is printed on a single-nozzle printer (frequent changes and purging). + BBLSingleExtruderMixedFilamentRisk, OrcaSharedProfilesAvailable, OrcaCloudAPIError, OrcaSyncConflict, diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 5e8ee31363..7bb3bec12b 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -6,6 +6,7 @@ #include #include #include "libslic3r/MultiNozzleUtils.hpp" +#include "libslic3r/FilamentMixer.hpp" #include #include #include @@ -1675,6 +1676,25 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const std::sort(plate_extruders.begin(), plate_extruders.end()); auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end()); plate_extruders.resize(std::distance(plate_extruders.begin(), it_end)); + + // Expand mixed filament slots to their physical components. A mixed slot is virtual and + // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the + // physical filaments it resolves to instead. + { + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* comp_strs_opt = project_config.option("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + std::vector ext_0based; + for (int e : plate_extruders) + if (e >= 1) ext_0based.push_back((unsigned int)(e - 1)); + auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values); + plate_extruders.clear(); + for (unsigned int e : expanded) + plate_extruders.push_back((int)(e + 1)); + } + } + return plate_extruders; } @@ -1836,6 +1856,24 @@ std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D std::sort(plate_extruders.begin(), plate_extruders.end()); auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end()); plate_extruders.resize(std::distance(plate_extruders.begin(), it_end)); + + // Expand mixed filament slots to their physical components. A mixed slot is virtual and + // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the + // physical filaments it resolves to instead. + { + auto* is_mixed_opt = full_config.option("filament_is_mixed"); + auto* comp_strs_opt = full_config.option("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + std::vector ext_0based; + for (int e : plate_extruders) + if (e >= 1) ext_0based.push_back((unsigned int)(e - 1)); + auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values); + plate_extruders.clear(); + for (unsigned int e : expanded) + plate_extruders.push_back((int)(e + 1)); + } + } + return plate_extruders; } @@ -1889,6 +1927,25 @@ std::vector PartPlate::get_extruders_without_support(bool conside_custom_gc std::sort(plate_extruders.begin(), plate_extruders.end()); auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end()); plate_extruders.resize(std::distance(plate_extruders.begin(), it_end)); + + // Expand mixed filament slots to their physical components. A mixed slot is virtual and + // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the + // physical filaments it resolves to instead. + { + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* comp_strs_opt = project_config.option("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + std::vector ext_0based; + for (int e : plate_extruders) + if (e >= 1) ext_0based.push_back((unsigned int)(e - 1)); + auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values); + plate_extruders.clear(); + for (unsigned int e : expanded) + plate_extruders.push_back((int)(e + 1)); + } + } + return plate_extruders; } @@ -1990,6 +2047,50 @@ bool PartPlate::check_tpu_printable_status(const DynamicPrintConfig & config, co return true; } +// A mixed-color filament alternates between its components constantly. On a single-nozzle +// printer every one of those switches is a full filament change plus a purge, so warn before +// slicing. Multi-nozzle printers keep the components loaded at once and are not affected. +bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const +{ + warning_text.clear(); + + auto *nozzle_diameter_opt = config.option("nozzle_diameter"); + if (!nozzle_diameter_opt || nozzle_diameter_opt->values.size() > 1) + return false; + + auto *is_mixed_opt = wxGetApp().preset_bundle->project_config.option("filament_is_mixed"); + if (!is_mixed_opt || !has_any_mixed_filament(is_mixed_opt->values)) + return false; + + auto is_mixed_slot = [&](int extruder_1based) { + size_t idx = (size_t)(extruder_1based - 1); + return idx < is_mixed_opt->values.size() && is_mixed_opt->values[idx]; + }; + + const std::string mixed_warn_msg = _u8L("Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, " + "which may significantly increase waste and the risk of nozzle / waste-chute clogging."); + + for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) { + if (!contain_instance_totally(obj_idx, 0)) + continue; + ModelObject *mo = m_model->objects[obj_idx]; + int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1; + if (is_mixed_slot(obj_ext)) { + warning_text = mixed_warn_msg; + return true; + } + for (ModelVolume *mv : mo->volumes) { + int vol_ext = mv->config.has("extruder") ? mv->config.extruder() : obj_ext; + if (is_mixed_slot(vol_ext)) { + warning_text = mixed_warn_msg; + return true; + } + } + } + + return false; +} + bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config) { bool has_pla = false; @@ -6384,6 +6485,31 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w } //parse filament info plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result()); + + // Record mixed (virtual) filaments actually used on this plate. + // Source is ToolOrdering::used_mixed_filaments (slots that appeared in + // layer tools before resolve), persisted on GCodeProcessorResult / Print — + // not print->extruders() which only reflects assignment. + { + std::vector used_mixed; + if (auto *slice_result = m_plate_list[i]->get_slice_result()) + used_mixed = slice_result->used_mixed_filaments; + if (used_mixed.empty() && print) + used_mixed = print->get_slice_used_mixed_filaments(); + if (!used_mixed.empty() && print) { + const auto &fila_types = print->config().filament_type.values; + const auto &fila_colors = print->config().filament_colour.values; + const auto &fila_comps = print->config().filament_mixed_components.values; + for (unsigned int fid : used_mixed) { + PlateMixedFilamentInfo mixed_info; + mixed_info.id = (int) fid + 1; + if (fid < fila_types.size()) mixed_info.type = fila_types[fid]; + if (fid < fila_colors.size()) mixed_info.color = fila_colors[fid]; + if (fid < fila_comps.size()) mixed_info.components = fila_comps[fid]; + plate_data_item->mixed_filaments_info.push_back(mixed_info); + } + } + } } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() << ", result valid = " << m_plate_list[i]->is_slice_result_valid(); @@ -6450,6 +6576,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info; gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; + gcode_result->used_mixed_filaments.clear(); + for (const auto &mixed_info : plate_data_list[i]->mixed_filaments_info) { + if (mixed_info.id > 0) + gcode_result->used_mixed_filaments.push_back(static_cast(mixed_info.id - 1)); + } + if (Print *print = dynamic_cast(fff_print)) + print->set_slice_used_mixed_filaments(gcode_result->used_mixed_filaments); // Reconstruct the device-side nozzle grouping from the loaded 3mf so // the monitor/preview can map filaments to physical nozzles. diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 47481dcad4..5760320b49 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -354,6 +354,9 @@ public: bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message); bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector &tpu_filaments); bool check_mixture_of_pla_and_petg(const DynamicPrintConfig & config); + // Warns when a mixed-color filament is used on a single-nozzle printer, where every + // component switch costs a full filament change and purge. + bool check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const; bool check_mixture_filament_compatible(const DynamicPrintConfig& config, std::string &error_msg); bool check_compatible_of_nozzle_and_filament(const DynamicPrintConfig & config, const std::vector& filament_presets, std::string& error_msg); diff --git a/src/slic3r/GUI/PlateSettingsDialog.cpp b/src/slic3r/GUI/PlateSettingsDialog.cpp index ea24d646c3..07c955ef01 100644 --- a/src/slic3r/GUI/PlateSettingsDialog.cpp +++ b/src/slic3r/GUI/PlateSettingsDialog.cpp @@ -472,6 +472,31 @@ PlateSettingsDialog::PlateSettingsDialog(wxWindow* parent, const wxString& title m_sizer_main->AddSpacer(FromDIP(5)); m_sizer_main->Add(m_other_layers_seq_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30)); + // A mixed-color slot resolves to a different physical filament per layer, so a user-defined + // filament order cannot be honoured; grey out the choice and explain that in the dialog. + { + auto &proj_cfg = wxGetApp().preset_bundle->project_config; + auto *is_mixed_opt = proj_cfg.option("filament_is_mixed"); + if (is_mixed_opt && Slic3r::has_any_mixed_filament(is_mixed_opt->values)) { + m_first_layer_print_seq_choice->Enable(false); + m_other_layers_seq_panel->enable_seq_choice(false); + + auto *warn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *warn_icon = new wxStaticBitmap(this, wxID_ANY, create_scaled_bitmap("warning", this, 16), + wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16))); + auto *warn_text = new wxStaticText(this, wxID_ANY, + _L("The filament list contains mixed filaments. Custom filament sequence will not take effect.")); + warn_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00"))); + warn_text->SetFont(Label::Body_12); + warn_text->Wrap(FromDIP(300)); + + warn_sizer->Add(warn_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + warn_sizer->Add(warn_text, 1, wxALIGN_CENTER_VERTICAL, 0); + m_sizer_main->AddSpacer(FromDIP(5)); + m_sizer_main->Add(warn_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30)); + } + } + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](auto& e) { diff --git a/src/slic3r/GUI/PlateSettingsDialog.hpp b/src/slic3r/GUI/PlateSettingsDialog.hpp index 1e61b0a708..b94739348f 100644 --- a/src/slic3r/GUI/PlateSettingsDialog.hpp +++ b/src/slic3r/GUI/PlateSettingsDialog.hpp @@ -62,6 +62,9 @@ public: int get_layers_print_seq_choice() { return m_other_layer_print_seq_choice->GetSelection(); }; std::vector get_layers_print_seq_infos() { return m_layer_seq_infos; } + // Lets callers grey out the sequence choice (e.g. when a mixed filament makes a + // user-defined filament order impossible). + void enable_seq_choice(bool enable) { m_other_layer_print_seq_choice->Enable(enable); } protected: void append_layer(const LayerSeqInfo* layer_info = nullptr); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c580f16c70..330574f1c0 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -61,7 +61,7 @@ #include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/STEP.hpp" #include "libslic3r/Format/AMF.hpp" -//#include "libslic3r/Format/3mf.hpp" +// #include "libslic3r/Format/3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Model.hpp" @@ -128,11 +128,11 @@ #include "NotificationManager.hpp" #include "PresetComboBoxes.hpp" #include "MsgDialog.hpp" -#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount -#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles +#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount +#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification -#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GizmoObjectManipulation.hpp" // BBS @@ -155,9 +155,15 @@ #endif // __APPLE__ #include -#include // Needs to be last because reasons :-/ +#include // Needs to be last because reasons :-/ #include #include "WipeTowerDialog.hpp" +#include "MixedFilamentDialog.hpp" +#include "TextureImportDialog.hpp" +#include "libslic3r/TexturePainting.hpp" +#include "ColorDecomposeSupport.hpp" +#include "FilamentBitmapUtils.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "ObjColorDialog.hpp" #include "libslic3r/CustomGCode.hpp" @@ -189,39 +195,52 @@ using Slic3r::Preset; using Slic3r::GUI::format_wxstr; using namespace nlohmann; -static const std::pair THUMBNAIL_SIZE_3MF = { 512, 512 }; +static const std::pair THUMBNAIL_SIZE_3MF = {512, 512}; -namespace Slic3r { -namespace GUI { +namespace Slic3r { namespace GUI { -wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); -wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); -wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); -wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); -wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); -wxDEFINE_EVENT(EVT_EXPORT_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_IMPORT_MODEL_ID, wxCommandEvent); -wxDEFINE_EVENT(EVT_DOWNLOAD_PROJECT, wxCommandEvent); -wxDEFINE_EVENT(EVT_PUBLISH, wxCommandEvent); -wxDEFINE_EVENT(EVT_OPEN_PLATESETTINGSDIALOG, wxCommandEvent); +// A textured mesh is only worth routing through the import dialog when it actually carries +// decoded image data; UV-only meshes have nothing to sample. +static bool has_importable_texture(const Slic3r::TexturedMesh& textured_mesh) +{ + if (textured_mesh.vertices.empty() || textured_mesh.indices.empty()) + return false; + + if (!textured_mesh.precomputed_face_colors.empty()) + return true; + + return std::any_of(textured_mesh.textures.begin(), textured_mesh.textures.end(), + [](const Slic3r::TextureImage& texture) { return !texture.data.empty(); }); +} + +wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); +wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); +wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); +wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); +wxDEFINE_EVENT(EVT_EXPORT_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_IMPORT_MODEL_ID, wxCommandEvent); +wxDEFINE_EVENT(EVT_DOWNLOAD_PROJECT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PUBLISH, wxCommandEvent); +wxDEFINE_EVENT(EVT_OPEN_PLATESETTINGSDIALOG, wxCommandEvent); wxDEFINE_EVENT(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, wxCommandEvent); // BBS: backup & restore -wxDEFINE_EVENT(EVT_RESTORE_PROJECT, wxCommandEvent); -wxDEFINE_EVENT(EVT_PRINT_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_SEND_CALIBRATION_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_SEND_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_PUBLISH_FINISHED, wxCommandEvent); -//BBS: repair model -wxDEFINE_EVENT(EVT_REPAIR_MODEL, wxCommandEvent); -wxDEFINE_EVENT(EVT_FILAMENT_COLOR_CHANGED, wxCommandEvent); -wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_NETWORKING, wxCommandEvent); -wxDEFINE_EVENT(EVT_UPDATE_PLUGINS_WHEN_LAUNCH, wxCommandEvent); -wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_HINT, wxCommandEvent); -wxDEFINE_EVENT(EVT_PREVIEW_ONLY_MODE_HINT, wxCommandEvent); -//BBS: change light/dark mode -wxDEFINE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); -//BBS: print -wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); +wxDEFINE_EVENT(EVT_RESTORE_PROJECT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PRINT_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_SEND_CALIBRATION_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_SEND_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PUBLISH_FINISHED, wxCommandEvent); +// BBS: repair model +wxDEFINE_EVENT(EVT_REPAIR_MODEL, wxCommandEvent); +wxDEFINE_EVENT(EVT_FILAMENT_COLOR_CHANGED, wxCommandEvent); +wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_NETWORKING, wxCommandEvent); +wxDEFINE_EVENT(EVT_UPDATE_PLUGINS_WHEN_LAUNCH, wxCommandEvent); +wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_HINT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PREVIEW_ONLY_MODE_HINT, wxCommandEvent); +// BBS: change light/dark mode +wxDEFINE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); +// BBS: print +wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDEFINE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); @@ -231,32 +250,31 @@ wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); wxDEFINE_EVENT(EVT_NOTICE_CHILDE_SIZE_CHANGED, SimpleEvent); wxDEFINE_EVENT(EVT_NOTICE_FULL_SCREEN_CHANGED, IntEvent); #define PRINTER_THUMBNAIL_SIZE (wxSize(40, 40)) // ORCA -#define PRINTER_PANEL_SIZE ( wxSize(70, 60)) // ORCA -#define PRINTER_PANEL_RADIUS (6) // ORCA +#define PRINTER_PANEL_SIZE (wxSize(70, 60)) // ORCA +#define PRINTER_PANEL_RADIUS (6) // ORCA #define BTN_SYNC_SIZE (wxSize(FromDIP(96), FromDIP(98))) static string get_diameter_string(float diameter) { std::ostringstream stream; // ORCA ensure 0.25 returned as 0.25. previous code returned as 0.2 because of std::setprecision(1) - stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably + stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably std::string s = stream.str(); - if (s.find('.') != std::string::npos) { // Remove trailing zeros, but keep at least one decimal if needed + if (s.find('.') != std::string::npos) { // Remove trailing zeros, but keep at least one decimal if needed s.erase(s.find_last_not_of('0') + 1); - if (s.back() == '.') s += '0'; // Ensure "1." → "1.0" + if (s.back() == '.') + s += '0'; // Ensure "1." → "1.0" } return s; } -template -static void set_config_values(DynamicPrintConfig *config, const std::string &key, T value) +template static void set_config_values(DynamicPrintConfig* config, const std::string& key, T value) { auto config_opt = config->option(key); if (config_opt) { for (size_t i = 0; i < config_opt->values.size(); ++i) { config_opt->values[i] = value; } - } - else { + } else { BOOST_LOG_TRIVIAL(info) << "set_config_values: the key" << key << "is empty."; } } @@ -278,36 +296,22 @@ bool Plater::has_illegal_filename_characters(const std::string& name) } void Plater::show_illegal_characters_warning(wxWindow* parent) -{ - show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\""); -} +{ show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\""); } -void Plater::mark_plate_toolbar_image_dirty() -{ - m_b_plate_toolbar_image_dirty = true; -} +void Plater::mark_plate_toolbar_image_dirty() { m_b_plate_toolbar_image_dirty = true; } -bool Plater::is_plate_toolbar_image_dirty() const -{ - return m_b_plate_toolbar_image_dirty; -} +bool Plater::is_plate_toolbar_image_dirty() const { return m_b_plate_toolbar_image_dirty; } -void Plater::clear_plate_toolbar_image_dirty() -{ - m_b_plate_toolbar_image_dirty = false; -} +void Plater::clear_plate_toolbar_image_dirty() { m_b_plate_toolbar_image_dirty = false; } -static std::map bed_type_thumbnails = { - {BedType::btPC, "bed_cool" }, - {BedType::btEP, "bed_engineering" }, - {BedType::btPEI, "bed_high_templ" }, - {BedType::btPTE, "bed_pei" }, - {BedType::btPCT, "bed_pei_cool" }, - {BedType::btSuperTack, "bed_cool_supertack" } -}; +static std::map bed_type_thumbnails = {{BedType::btPC, "bed_cool"}, + {BedType::btEP, "bed_engineering"}, + {BedType::btPEI, "bed_high_templ"}, + {BedType::btPTE, "bed_pei"}, + {BedType::btPCT, "bed_pei_cool"}, + {BedType::btSuperTack, "bed_cool_supertack"}}; -enum SlicedInfoIdx -{ +enum SlicedInfoIdx { siFilament_m, siFilament_mm3, siFilament_g, @@ -327,37 +331,30 @@ enum class LoadFilesType { Multiple3MFOther, }; -enum class LoadType : unsigned char -{ - Unknown, - OpenProject, - LoadGeometry, - LoadConfig -}; +enum class LoadType : unsigned char { Unknown, OpenProject, LoadGeometry, LoadConfig }; class SlicedInfo : public wxStaticBoxSizer { public: - SlicedInfo(wxWindow *parent); - void SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label=""); + SlicedInfo(wxWindow* parent); + void SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label = ""); private: std::vector> info_vec; }; -SlicedInfo::SlicedInfo(wxWindow *parent) : - wxStaticBoxSizer(new wxStaticBox(parent, wxID_ANY, _L("Sliced Info")), wxVERTICAL) +SlicedInfo::SlicedInfo(wxWindow* parent) : wxStaticBoxSizer(new wxStaticBox(parent, wxID_ANY, _L("Sliced Info")), wxVERTICAL) { GetStaticBox()->SetFont(wxGetApp().bold_font()); wxGetApp().UpdateDarkUI(GetStaticBox()); - auto *grid_sizer = new wxFlexGridSizer(2, 5, 15); + auto* grid_sizer = new wxFlexGridSizer(2, 5, 15); grid_sizer->SetFlexibleDirection(wxVERTICAL); info_vec.reserve(siCount); auto init_info_label = [this, parent, grid_sizer](wxString text_label) { - auto *text = new wxStaticText(parent, wxID_ANY, text_label); + auto* text = new wxStaticText(parent, wxID_ANY, text_label); text->SetForegroundColour(*wxBLACK); text->SetFont(wxGetApp().small_font()); auto info_label = new wxStaticText(parent, wxID_ANY, "N/A"); @@ -380,7 +377,7 @@ SlicedInfo::SlicedInfo(wxWindow *parent) : this->Show(false); } -void SlicedInfo::SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label/*=""*/) +void SlicedInfo::SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label /*=""*/) { const bool show = text != "N/A"; if (show) @@ -405,9 +402,8 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove } static const std::regex state_pattern(R"(state=(\d+);)"); - constexpr unsigned int disabled_wayland_flags = - static_cast(wxAuiPaneInfo::optionFloating) | - static_cast(wxAuiPaneInfo::optionFloatable); + constexpr unsigned int disabled_wayland_flags = static_cast(wxAuiPaneInfo::optionFloating) | + static_cast(wxAuiPaneInfo::optionFloatable); const std::string input = layout.utf8_string(); std::string output; @@ -421,9 +417,9 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove output.append(search_start, match[0].first); try { - const unsigned long state = std::stoul(match[1].str()); + const unsigned long state = std::stoul(match[1].str()); const unsigned long sanitized_state = state & ~static_cast(disabled_wayland_flags); - modified = modified || sanitized_state != state; + modified = modified || sanitized_state != state; output += "state=" + std::to_string(sanitized_state) + ";"; } catch (const std::exception&) { @@ -446,11 +442,7 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove // Sidebar / private -enum class ActionButtonType : int { - abReslice, - abExport, - abSendGCode -}; +enum class ActionButtonType : int { abReslice, abExport, abSendGCode }; // Background for the extruder-group title chip and its edit buttons, matching the StaticGroup // interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour. @@ -469,7 +461,7 @@ static wxColour extruder_group_chip_bg() class HoverLabel : public wxPanel { public: - HoverLabel(wxWindow *parent, const wxString &label) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + HoverLabel(wxWindow* parent, const wxString& label) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) { SetBackgroundColour(extruder_group_chip_bg()); auto sizer = new wxBoxSizer(wxHORIZONTAL); @@ -496,7 +488,7 @@ public: m_hover_btn = new ScalableButton(this, wxID_ANY, "dot"); m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1)); m_hover_btn->SetBackgroundColour(extruder_group_chip_bg()); - m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { + m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto& evt) { if (m_enabled && m_hover_on_click) m_hover_on_click(); }); @@ -537,7 +529,7 @@ public: UpdateSizing(); } - void SetTitle(const wxString &title) + void SetTitle(const wxString& title) { m_label->SetLabel(title); UpdateSizing(); @@ -550,7 +542,7 @@ public: { SetBackgroundColour(extruder_group_chip_bg()); m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); - for (wxStaticText *t : {m_brace_left, m_count, m_brace_right}) + for (wxStaticText* t : {m_brace_left, m_count, m_brace_right}) t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); m_hover_btn->SetBackgroundColour(extruder_group_chip_bg()); Refresh(); @@ -568,36 +560,36 @@ private: GetParent()->Layout(); } - wxStaticText *m_label; - wxStaticText *m_brace_left; - wxStaticText *m_count; - wxStaticText *m_brace_right; - ScalableButton *m_hover_btn; + wxStaticText* m_label; + wxStaticText* m_brace_left; + wxStaticText* m_count; + wxStaticText* m_brace_right; + ScalableButton* m_hover_btn; std::function m_hover_on_click; - bool m_enabled{false}; + bool m_enabled{false}; }; struct ExtruderGroup : StaticGroup { - ExtruderGroup(wxWindow * parent, int index, wxString const &title); - wxStaticBoxSizer *sizer = nullptr; - HoverLabel * hover_label = nullptr; - ScalableButton * btn_edit = nullptr; - ComboBox * combo_diameter = nullptr; - ComboBox * combo_flow = nullptr; - AMSPreview * ams[4] = {nullptr}; - wxStaticText *ams_not_installed_msg{nullptr}; - ScalableButton * btn_up{nullptr}; - ScalableButton * btn_down{nullptr}; - wxBoxSizer *hsizer_ams { nullptr }; - size_t page_cur{0}; - size_t page_num{3}; - size_t ams_n4 = 0; - size_t ams_n1 = 0; + ExtruderGroup(wxWindow* parent, int index, wxString const& title); + wxStaticBoxSizer* sizer = nullptr; + HoverLabel* hover_label = nullptr; + ScalableButton* btn_edit = nullptr; + ComboBox* combo_diameter = nullptr; + ComboBox* combo_flow = nullptr; + AMSPreview* ams[4] = {nullptr}; + wxStaticText* ams_not_installed_msg{nullptr}; + ScalableButton* btn_up{nullptr}; + ScalableButton* btn_down{nullptr}; + wxBoxSizer* hsizer_ams{nullptr}; + size_t page_cur{0}; + size_t page_num{3}; + size_t ams_n4 = 0; + size_t ams_n1 = 0; std::vector ams_4; std::vector ams_1; - wxString diameter; + wxString diameter; void set_ams_count(int n4, int n1) { @@ -612,11 +604,23 @@ struct ExtruderGroup : StaticGroup void update_ams(); void SetTitle(const wxString& title); - void SetCount(int count) { if (hover_label) hover_label->SetCount(count); } - void SetEditEnabled(bool enable) { if (hover_label) hover_label->EnableEdit(enable); } - void SetOnHoverClick(std::function on_click) { if (hover_label) hover_label->SetOnHoverClick(std::move(on_click)); } + void SetCount(int count) + { + if (hover_label) + hover_label->SetCount(count); + } + void SetEditEnabled(bool enable) + { + if (hover_label) + hover_label->EnableEdit(enable); + } + void SetOnHoverClick(std::function on_click) + { + if (hover_label) + hover_label->SetOnHoverClick(std::move(on_click)); + } - void sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector const &ams1); + void sync_ams(MachineObject const* obj, std::vector const& ams4, std::vector const& ams1); void Rescale() { @@ -644,117 +648,136 @@ struct ExtruderGroup : StaticGroup struct Sidebar::priv { - Plater *plater; + Plater* plater; - wxPanel *scrolled = nullptr; - PlaterPresetComboBox *combo_sla_print = nullptr; - PlaterPresetComboBox *combo_sla_material = nullptr; + wxPanel* scrolled = nullptr; + PlaterPresetComboBox* combo_sla_print = nullptr; + PlaterPresetComboBox* combo_sla_material = nullptr; // Printer - wxSizer * vsizer_printer = nullptr; - wxBoxSizer * extruder_dual_sizer = nullptr; - wxBoxSizer * extruder_single_sizer = nullptr; + wxSizer* vsizer_printer = nullptr; + wxBoxSizer* extruder_dual_sizer = nullptr; + wxBoxSizer* extruder_single_sizer = nullptr; // Printer - preset - StaticBox * panel_printer_preset = nullptr; - wxStaticBitmap * image_printer = nullptr; - PlaterPresetComboBox *combo_printer = nullptr; - ScalableButton * btn_edit_printer = nullptr; - ScalableButton * btn_connect_printer = nullptr; + StaticBox* panel_printer_preset = nullptr; + wxStaticBitmap* image_printer = nullptr; + PlaterPresetComboBox* combo_printer = nullptr; + ScalableButton* btn_edit_printer = nullptr; + ScalableButton* btn_connect_printer = nullptr; // Nozzle diameter - StaticBox * panel_nozzle_dia = nullptr; - Label * label_nozzle_title= nullptr; - ComboBox * combo_nozzle_dia = nullptr; - Label * label_nozzle_type = nullptr; + StaticBox* panel_nozzle_dia = nullptr; + Label* label_nozzle_title = nullptr; + ComboBox* combo_nozzle_dia = nullptr; + Label* label_nozzle_type = nullptr; // Printer - bed - StaticBox * panel_printer_bed = nullptr; - wxStaticBitmap *image_printer_bed = nullptr; - ComboBox * combo_printer_bed = nullptr; + StaticBox* panel_printer_bed = nullptr; + wxStaticBitmap* image_printer_bed = nullptr; + ComboBox* combo_printer_bed = nullptr; - ImageDPIFrame *big_bed_image_popup = nullptr; + ImageDPIFrame* big_bed_image_popup = nullptr; // Printer - sync - //Button *btn_sync_printer; + // Button *btn_sync_printer; std::shared_ptr counter_sync_printer = std::make_shared(); - wxTimer * timer_sync_printer = new wxTimer(); + wxTimer* timer_sync_printer = new wxTimer(); // Printer - ams - ExtruderGroup *left_extruder = nullptr; - ExtruderGroup *right_extruder = nullptr; - ExtruderGroup *single_extruder = nullptr; + ExtruderGroup* left_extruder = nullptr; + ExtruderGroup* right_extruder = nullptr; + ExtruderGroup* single_extruder = nullptr; - int FromDIP(int n) { return plater->FromDIP(n); } + int FromDIP(int n) { return plater->FromDIP(n); } void layout_printer(bool isBBL, bool isDual); void flush_printer_sync(bool restart = false); - PlaterPresetComboBox *combo_print = nullptr; + PlaterPresetComboBox* combo_print = nullptr; std::vector combos_filament; - int editing_filament = -1; - wxBoxSizer *sizer_filaments = nullptr; + int editing_filament = -1; + wxBoxSizer* sizer_filaments = nullptr; - //BBS Sidebar widgets + // BBS Sidebar widgets wxPanel* m_panel_print_title; wxStaticText* m_staticText_print_title; wxPanel* m_panel_print_content; - wxBoxSizer *sizer_params; + wxBoxSizer* sizer_params; - //wxComboBox * m_comboBox_print_preset; - wxStaticLine * m_staticline1; + // wxComboBox * m_comboBox_print_preset; + wxStaticLine* m_staticline1; StaticBox* m_panel_filament_title; - wxPanel* m_panel_filament_separator; + wxPanel* m_panel_filament_separator; wxStaticText* m_staticText_filament_settings; wxStaticText* m_staticText_filament_count; - ScalableButton * m_bpButton_add_filament; - ScalableButton * m_bpButton_del_filament; - ScalableButton * m_bpButton_ams_filament; - ScalableButton * m_bpButton_set_filament; + ScalableButton* m_bpButton_add_filament; + ScalableButton* m_bpButton_del_filament; + ScalableButton* m_bpButton_ams_filament; + ScalableButton* m_bpButton_set_filament; int m_menu_filament_id = -1; + + wxPanel* m_filament_area_wrapper; + wxScrolledWindow* m_panel_filament_content; + + // Mixed-color filament section. Sits directly under the physical filament list in + // scrolled_sizer. BBS hosts the equivalent widgets inside an m_filament_area_wrapper + // that Orca's sidebar has no counterpart for, so these are parented to p->scrolled. + wxPanel* m_btn_add_mixed_filament{nullptr}; // "+ Add Mixed Filament" full-width button + wxPanel* m_panel_mixed_title{nullptr}; // title row: "Mixed Filament" + add/del buttons + wxStaticText* m_text_mixed_title{nullptr}; + ScalableButton* m_btn_mixed_add{nullptr}; + ScalableButton* m_btn_mixed_del{nullptr}; + wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows + wxPanel* m_panel_mixed_content{nullptr}; + wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments + wxPanel* m_panel_mixed_warning{nullptr}; // red bar for broken/mismatched mixes + wxStaticText* m_text_mixed_warning{nullptr}; + bool m_mixed_filament_broken{false}; + wxScrolledWindow* m_scrolledWindow_filament_content; wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; - Button * m_purge_mode_btn = nullptr; - Button * m_flushing_volume_btn = nullptr; - TextInput* m_search_item = nullptr; - StaticBox* m_search_bar = nullptr; + Button* m_purge_mode_btn = nullptr; + Button* m_flushing_volume_btn = nullptr; + TextInput* m_search_item = nullptr; + StaticBox* m_search_bar = nullptr; Search::SearchObjectDialog* dia = nullptr; // BBS printer config - StaticBox* m_panel_printer_title = nullptr; - wxPanel* m_panel_printer_separator = nullptr; - ScalableButton* m_printer_icon = nullptr; - ScalableButton* m_printer_connect = nullptr; - ScalableButton* m_printer_bbl_sync = nullptr; - ScalableButton* m_printer_setting = nullptr; - wxStaticText * m_text_printer_settings = nullptr; - wxPanel* m_panel_printer_content = nullptr; + StaticBox* m_panel_printer_title = nullptr; + wxPanel* m_panel_printer_separator = nullptr; + ScalableButton* m_printer_icon = nullptr; + ScalableButton* m_printer_connect = nullptr; + ScalableButton* m_printer_bbl_sync = nullptr; + ScalableButton* m_printer_setting = nullptr; + wxStaticText* m_text_printer_settings = nullptr; + wxPanel* m_panel_printer_content = nullptr; // Filament Track Switch status overlay: an icon floated over the left/single extruder AMS area, // shown only when the switch is installed (green when ready, red when not calibrated). wxStaticBitmap* extruder_separator_icon = nullptr; - ObjectList *m_object_list{ nullptr }; - ObjectSettings *object_settings{ nullptr }; - ObjectLayers *object_layers{ nullptr }; + ObjectList* m_object_list{nullptr}; + ObjectSettings* object_settings{nullptr}; + ObjectLayers* object_layers{nullptr}; - wxButton *btn_export_gcode; - wxButton *btn_reslice; - ScalableButton *btn_send_gcode; - //ScalableButton *btn_eject_device; - ScalableButton* btn_export_gcode_removable; //exports to removable drives (appears only if removable drive is connected) + wxButton* btn_export_gcode; + wxButton* btn_reslice; + ScalableButton* btn_send_gcode; + // ScalableButton *btn_eject_device; + ScalableButton* btn_export_gcode_removable; // exports to removable drives (appears only if removable drive is connected) - bool is_switching_diameter{false}; - Search::OptionsSearcher searcher; + bool is_switching_diameter{false}; + Search::OptionsSearcher searcher; std::string ams_list_device; - priv(Plater *plater) : plater(plater) {} + priv(Plater* plater) : plater(plater) {} ~priv(); void show_preset_comboboxes(); void jump_to_object(ObjectDataViewModelNode* item); void can_search(); - bool sync_extruder_list(bool &only_external_material, bool is_manual = false); + bool sync_extruder_list(bool& only_external_material, bool is_manual = false); // Resolve the nozzle option for a multi-nozzle machine. Returns nullopt (and is a no-op) unless // extruder_count >= 2 && support_multi_nozzle. When is_manual, always pops the MultiNozzleSyncDialog; // otherwise reuses the app_config-cached option when the machine's nozzle config is unchanged. @@ -785,17 +808,16 @@ struct Sidebar::priv void Sidebar::priv::layout_printer(bool isBBL, bool isDual) { // Printer - preset - if (auto sizer = static_cast(panel_printer_preset->GetSizer()); - sizer == nullptr /*|| isBBL != (sizer->GetOrientation() == wxVERTICAL)*/) { - - //if (isBBL) { - wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); - hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); - hsizer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(2)); // 1 already triggers wxEXPAND - hsizer->AddSpacer(FromDIP(2)); - hsizer->Add(btn_edit_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); - //hsizer->Add(btn_connect_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); - panel_printer_preset->SetSizer(hsizer); + if (auto sizer = static_cast(panel_printer_preset->GetSizer()); + sizer == nullptr /*|| isBBL != (sizer->GetOrientation() == wxVERTICAL)*/) { + // if (isBBL) { + wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); + hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + hsizer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(2)); // 1 already triggers wxEXPAND + hsizer->AddSpacer(FromDIP(2)); + hsizer->Add(btn_edit_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); + // hsizer->Add(btn_connect_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); + panel_printer_preset->SetSizer(hsizer); //} else { // wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); // hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_CENTER, FromDIP(4)); @@ -807,11 +829,11 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) } if (vsizer_printer->GetItemCount() == 0) { - wxBoxSizer *hsizer_printer = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* hsizer_printer = new wxBoxSizer(wxHORIZONTAL); hsizer_printer->Add(panel_printer_preset, 1, wxEXPAND, 0); - hsizer_printer->Add(panel_nozzle_dia , 0, wxLEFT, FromDIP(4)); + hsizer_printer->Add(panel_nozzle_dia, 0, wxLEFT, FromDIP(4)); hsizer_printer->Add(panel_printer_bed, 0, wxLEFT, FromDIP(4)); - //hsizer_printer->Add(btn_sync_printer , 0, wxLEFT, FromDIP(4)); + // hsizer_printer->Add(btn_sync_printer , 0, wxLEFT, FromDIP(4)); vsizer_printer->AddSpacer(FromDIP(SidebarProps::ContentMarginV())); vsizer_printer->Add(hsizer_printer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); @@ -826,8 +848,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) // Filament Track Switch status icon, floated over the extruder AMS area (positioned in // update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip. if (!extruder_separator_icon) { - auto bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch", 10); - extruder_separator_icon = new wxStaticBitmap(m_panel_printer_content, wxID_ANY, bitmap.bmp(), wxDefaultPosition, bitmap.GetBmpSize()); + auto bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch", 10); + extruder_separator_icon = new wxStaticBitmap(m_panel_printer_content, wxID_ANY, bitmap.bmp(), wxDefaultPosition, + bitmap.GetBmpSize()); extruder_separator_icon->Hide(); extruder_separator_icon->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& evt) { show_fila_switch_msg(is_fila_switch_ready()); @@ -836,9 +859,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) } // single - extruder_single_sizer = single_extruder->sizer; - wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL); - extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); + extruder_single_sizer = single_extruder->sizer; + wxBoxSizer* extruder_sizer = new wxBoxSizer(wxVERTICAL); + extruder_sizer->Add(extruder_dual_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); vsizer_printer->Add(extruder_sizer, 1, wxEXPAND | wxTOP, FromDIP(2)); @@ -846,14 +869,14 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) vsizer_printer->AddSpacer(FromDIP(SidebarProps::ContentMarginV())); } - //btn_connect_printer->Show(!isBBL); + // btn_connect_printer->Show(!isBBL); m_printer_connect->Show(!isBBL); - //btn_sync_printer->Show(isBBL); + // btn_sync_printer->Show(isBBL); m_printer_bbl_sync->Show(isBBL); // ORCA show plate type combo box only when its supported - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - const auto& cfg = preset_bundle.printers.get_edited_preset().config; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + const auto& cfg = preset_bundle.printers.get_edited_preset().config; // Orca: we use preset_bundle.is_bbl_vendor() instead of isBBL to determine if the plate type combo box should be shown // ref: https://github.com/OrcaSlicer/OrcaSlicer/pull/11610#discussion_r2607411847 panel_printer_bed->Show(preset_bundle.is_bbl_vendor() || cfg.opt_bool("support_multi_bed_types")); @@ -864,7 +887,7 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) // Single nozzle & non ams if (!isDual) { // Orca: for printer without flow variant, we do not show flow combo - int extruder_count = 0; + int extruder_count = 0; const bool has_flow_variant = cfg.support_different_extruders(extruder_count); panel_nozzle_dia->Show(!has_flow_variant); @@ -876,9 +899,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) // ORCA ensure printer section is visible after changing printer from printer selection dialog // this will inform user on printer change when printer section is collapsed - if (m_panel_printer_content){ + if (m_panel_printer_content) { bool isShown = m_panel_printer_content->IsShown(); - if(!isShown && m_text_printer_settings){ + if (!isShown && m_text_printer_settings) { m_text_printer_settings->SetLabel(_L("Printer")); // ensure title returns to default state m_panel_printer_content->Show(); } @@ -891,7 +914,7 @@ void Sidebar::priv::flush_printer_sync(bool restart) *counter_sync_printer = 6; timer_sync_printer->Start(500); } - //btn_sync_printer->SetBackgroundColorNormal((*counter_sync_printer & 1) ? "#F8F8F8" :"#009688"); + // btn_sync_printer->SetBackgroundColorNormal((*counter_sync_printer & 1) ? "#F8F8F8" :"#009688"); m_printer_bbl_sync->SetBitmap_((*counter_sync_printer & 1) ? "printer_sync_not" : "printer_sync_ok"); if (--*counter_sync_printer <= 0) timer_sync_printer->Stop(); @@ -900,7 +923,7 @@ void Sidebar::priv::flush_printer_sync(bool restart) Sidebar::priv::~priv() { // BBS - //delete object_manipulation; + // delete object_manipulation; delete object_settings; // BBS #if 0 @@ -912,7 +935,7 @@ void Sidebar::priv::show_preset_comboboxes() { const bool showSLA = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA; -//BBS +// BBS #if 0 for (size_t i = 0; i < 4; ++i) sizer_presets->Show(i, !showSLA); @@ -929,10 +952,7 @@ void Sidebar::priv::show_preset_comboboxes() scrolled->Refresh(); } -void Sidebar::priv::jump_to_object(ObjectDataViewModelNode* item) -{ - m_object_list->selected_object(item); -} +void Sidebar::priv::jump_to_object(ObjectDataViewModelNode* item) { m_object_list->selected_object(item); } void Sidebar::priv::can_search() { @@ -983,61 +1003,75 @@ void Sidebar::priv::hide_rich_tip(wxButton* btn) } #endif -std::vector get_min_flush_volumes(const DynamicPrintConfig &full_config, size_t nozzle_id) +std::vector get_min_flush_volumes(const DynamicPrintConfig& full_config, size_t nozzle_id) { - std::vectorextra_flush_volumes; - //const auto& full_config = wxGetApp().preset_bundle->full_config(); - //auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + std::vector extra_flush_volumes; + // const auto& full_config = wxGetApp().preset_bundle->full_config(); + // auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; const ConfigOptionFloatsNullable* nozzle_volume_opt = full_config.option("nozzle_volume"); - int nozzle_volume_val = nozzle_volume_opt ? (int)nozzle_volume_opt->get_at(nozzle_id) : 0; + int nozzle_volume_val = nozzle_volume_opt ? (int) nozzle_volume_opt->get_at(nozzle_id) : 0; const ConfigOptionInt* enable_long_retraction_when_cut_opt = full_config.option("enable_long_retraction_when_cut"); - int machine_enabled_level = 0; + int machine_enabled_level = 0; if (enable_long_retraction_when_cut_opt) { machine_enabled_level = enable_long_retraction_when_cut_opt->value; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get enable_long_retraction_when_cut from config, value=%1%")%machine_enabled_level; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": get enable_long_retraction_when_cut from config, value=%1%") % machine_enabled_level; } const ConfigOptionBools* long_retractions_when_cut_opt = full_config.option("long_retractions_when_cut"); - bool machine_activated = false; + bool machine_activated = false; if (long_retractions_when_cut_opt) { machine_activated = long_retractions_when_cut_opt->values[nozzle_id] == 1; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get long_retractions_when_cut from config, value=%1%, activated=%2%")%long_retractions_when_cut_opt->values[0] %machine_activated; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": get long_retractions_when_cut from config, value=%1%, activated=%2%") % + long_retractions_when_cut_opt->values[0] % machine_activated; } size_t filament_size = full_config.option("filament_diameter")->values.size(); - std::vector filament_retraction_distance_when_cut(filament_size, 18.0f), printer_retraction_distance_when_cut(filament_size, 18.0f); + std::vector filament_retraction_distance_when_cut(filament_size, 18.0f), + printer_retraction_distance_when_cut(filament_size, 18.0f); std::vector filament_long_retractions_when_cut(filament_size, 0); - const ConfigOptionFloats* filament_retraction_distances_when_cut_opt = full_config.option("filament_retraction_distances_when_cut"); + const ConfigOptionFloats* filament_retraction_distances_when_cut_opt = full_config.option( + "filament_retraction_distances_when_cut"); if (filament_retraction_distances_when_cut_opt) { filament_retraction_distance_when_cut = filament_retraction_distances_when_cut_opt->values; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get filament_retraction_distance_when_cut from config, size=%1%, values=%2%")%filament_retraction_distance_when_cut.size() %filament_retraction_distances_when_cut_opt->serialize(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": get filament_retraction_distance_when_cut from config, size=%1%, values=%2%") % + filament_retraction_distance_when_cut.size() % + filament_retraction_distances_when_cut_opt->serialize(); } - const ConfigOptionFloats* printer_retraction_distance_when_cut_opt = full_config.option("retraction_distances_when_cut"); + const ConfigOptionFloats* printer_retraction_distance_when_cut_opt = full_config.option( + "retraction_distances_when_cut"); if (printer_retraction_distance_when_cut_opt) { printer_retraction_distance_when_cut = printer_retraction_distance_when_cut_opt->values; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get retraction_distances_when_cut from config, size=%1%, values=%2%")%printer_retraction_distance_when_cut.size() %printer_retraction_distance_when_cut_opt->serialize(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": get retraction_distances_when_cut from config, size=%1%, values=%2%") % + printer_retraction_distance_when_cut.size() % printer_retraction_distance_when_cut_opt->serialize(); } - const ConfigOptionBools* filament_long_retractions_when_cut_opt = full_config.option("filament_long_retractions_when_cut"); + const ConfigOptionBools* filament_long_retractions_when_cut_opt = full_config.option( + "filament_long_retractions_when_cut"); if (filament_long_retractions_when_cut_opt) { filament_long_retractions_when_cut = filament_long_retractions_when_cut_opt->values; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get filament_long_retractions_when_cut from config, size=%1%, values=%2%")%filament_long_retractions_when_cut.size() %filament_long_retractions_when_cut_opt->serialize(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": get filament_long_retractions_when_cut from config, size=%1%, values=%2%") % + filament_long_retractions_when_cut.size() % filament_long_retractions_when_cut_opt->serialize(); } for (size_t idx = 0; idx < filament_size; ++idx) { int extra_flush_volume = nozzle_volume_val; - int retract_length = machine_enabled_level && machine_activated ? printer_retraction_distance_when_cut[nozzle_id] : 0; + int retract_length = machine_enabled_level && machine_activated ? printer_retraction_distance_when_cut[nozzle_id] : 0; unsigned char filament_activated = filament_long_retractions_when_cut[idx]; - double filament_retract_length = filament_retraction_distance_when_cut[idx]; + double filament_retract_length = filament_retraction_distance_when_cut[idx]; if (filament_activated == 0) retract_length = 0; else if (filament_activated == 1 && machine_enabled_level == LongRectrationLevel::EnableFilament) { if (!std::isnan(filament_retract_length)) - retract_length = (int)filament_retraction_distance_when_cut[idx]; + retract_length = (int) filament_retraction_distance_when_cut[idx]; else retract_length = printer_retraction_distance_when_cut[nozzle_id]; } @@ -1052,23 +1086,38 @@ std::vector get_min_flush_volumes(const DynamicPrintConfig &full_config, si struct DynamicFilamentList : DynamicList { - std::vector> items; + // Orca: support and wipe-tower keys are consumed by the engine without per-layer mixed + // resolution (see ConfigManipulation::update_print_fff_config), so their dropdowns list + // physical slots only; the per-feature *_filament_id keys keep every slot. BBS uses one + // physical-only list for all of its keys. + explicit DynamicFilamentList(bool physical_only = false) : physical_only(physical_only) {} + bool physical_only; + std::vector> items; + std::vector slot_map{0}; // combo index -> 1-based filament slot; slot_map[0] = 0 is "Default" - void apply_on(Choice *c) override + void apply_on(Choice* c) override { + if (!c) + return; if (items.empty()) update(true); - auto cb = dynamic_cast(c->window); + auto cb = dynamic_cast(c->window); + if (!cb) + return; wxString old_selection = cb->GetStringSelection(); - int old_index = cb->GetSelection(); + int old_index = cb->GetSelection(); + // slot_map is already rebuilt here: restoring through it keeps the index of every slot + // still listed and sends a vanished slot to the fallback below. + int old_slot = old_index >= 0 && old_index < int(slot_map.size()) ? slot_map[old_index] : -1; cb->Clear(); cb->Append(_L("Default")); for (auto i : items) { cb->Append(i.first, i.second ? *i.second : wxNullBitmap); } - if (old_index >= 0 && (unsigned int) old_index < cb->GetCount()) { - cb->SetSelection(old_index); + int restored = index_of(wxString::Format("%d", old_slot)); + if (restored > 0 || old_slot == 0) { + cb->SetSelection(restored); return; } @@ -1084,27 +1133,36 @@ struct DynamicFilamentList : DynamicList wxString get_value(int index) override { wxString str; - str << index; + str << (index >= 0 && index < int(slot_map.size()) ? slot_map[index] : 0); return str; } int index_of(wxString value) override { long n = 0; - return (value.ToLong(&n) && n <= items.size()) ? int(n) : -1; + if (!value.ToLong(&n)) + return -1; + for (int i = 0; i < int(slot_map.size()); ++i) + if (slot_map[i] == int(n)) + return i; + return 0; } void update(bool force = false) { items.clear(); + slot_map.assign(1, 0); if (!force && m_choices.empty()) return; - auto icons = get_extruder_color_icons(true); + auto icons = get_extruder_color_icons(true); auto presets = wxGetApp().preset_bundle->filament_presets; for (int i = 0; i < presets.size(); ++i) { + if (physical_only && wxGetApp().preset_bundle->is_mixed_filament(i)) + continue; wxString str; std::string type; wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); str << type; items.push_back({str, i < icons.size() ? icons[i] : nullptr}); + slot_map.push_back(i + 1); } DynamicList::update(); } @@ -1118,33 +1176,32 @@ static bool has_junction_deviation(const DynamicPrintConfig* printer_config) } const auto gcode_flavor = printer_config->option>("gcode_flavor"); const auto junction_dev = printer_config->option("machine_max_junction_deviation"); - return gcode_flavor && - gcode_flavor->value == GCodeFlavor::gcfMarlinFirmware && - junction_dev && - !junction_dev->values.empty() && + return gcode_flavor && gcode_flavor->value == GCodeFlavor::gcfMarlinFirmware && junction_dev && !junction_dev->values.empty() && junction_dev->values.front() > 0.0; } -static DynamicFilamentList dynamic_filament_list; +static DynamicFilamentList dynamic_filament_list; // every slot, mixed included (per-feature *_filament_id keys) +static DynamicFilamentList dynamic_physical_filament_list(true); // physical slots only (support_*, wipe_tower_filament) class AMSCountPopupWindow : public PopupWindow { public: - AMSCountPopupWindow(ExtruderGroup *extruder, int index) - : PopupWindow(extruder, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + AMSCountPopupWindow(ExtruderGroup* extruder, int index) : PopupWindow(extruder, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) { SetBackgroundColour(*wxWHITE); - auto msg = new wxStaticText(this, wxID_ANY, _L("Set the number of AMS installed on the nozzle.")); + auto msg = new wxStaticText(this, wxID_ANY, _L("Set the number of AMS installed on the nozzle.")); msg->SetFont(Label::Body_14); msg->SetForegroundColour("#262E30"); msg->Wrap(FromDIP(280)); auto box = new StaticBox(this, wxID_ANY); box->SetBackgroundColor(0xF8F8F8); box->SetBorderWidth(0); - auto img4 = new ScalableButton(box, wxID_ANY, "ams_4_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 44); - //img4->SetBackgroundColour(*wxWHITE); - auto img1 = new ScalableButton(box, wxID_ANY, "ams_1_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 44); - //img1->SetBackgroundColour(*wxWHITE); + auto img4 = new ScalableButton(box, wxID_ANY, "ams_4_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, + false, 44); + // img4->SetBackgroundColour(*wxWHITE); + auto img1 = new ScalableButton(box, wxID_ANY, "ams_1_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, + false, 44); + // img1->SetBackgroundColour(*wxWHITE); auto txt4 = new wxStaticText(box, wxID_ANY, _L("AMS(4 slots)")); txt4->SetFont(Label::Body_14); txt4->SetBackgroundColour(0xF8F8F8); @@ -1159,28 +1216,28 @@ public: GetAMSCount(1 - index, oth4, oth1); auto val4 = new SpinInput(box, {}, {}, wxDefaultPosition, {FromDIP(60), -1}, 0, 0, 4 - oth4, ams4); auto val1 = new SpinInput(box, {}, {}, wxDefaultPosition, {FromDIP(60), -1}, 0, 0, 8 - oth1, ams1); - auto event_handler = [index, val4, val1, extruder](auto &evt) { + auto event_handler = [index, val4, val1, extruder](auto& evt) { SetAMSCount(index, val4->GetValue(), val1->GetValue()); UpdateAMSCount(index, extruder); }; val4->Bind(wxEVT_SPINCTRL, event_handler); val1->Bind(wxEVT_SPINCTRL, event_handler); - wxSizer * sizer = new wxBoxSizer(wxVERTICAL); + wxSizer* sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(msg, 0, wxTOP | wxLEFT | wxRIGHT, FromDIP(10)); - wxSizer *sizer2 = new wxBoxSizer(wxVERTICAL); - wxSizer *sizer21 = new wxBoxSizer(wxHORIZONTAL); - sizer21->Add(img4, 0, wxALIGN_CENTRE); - sizer21->Add(txt4, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer21->Add(val4, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer2->Add(sizer21, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, FromDIP(14)); - sizer2->AddSpacer(FromDIP(6)); - wxSizer *sizer22 = new wxBoxSizer(wxHORIZONTAL); - sizer22->Add(img1, 0, wxALIGN_CENTRE); - sizer22->Add(txt1, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer22->Add(val1, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer2->Add(sizer22, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, FromDIP(14)); - box->SetSizer(sizer2); + wxSizer* sizer2 = new wxBoxSizer(wxVERTICAL); + wxSizer* sizer21 = new wxBoxSizer(wxHORIZONTAL); + sizer21->Add(img4, 0, wxALIGN_CENTRE); + sizer21->Add(txt4, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer21->Add(val4, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer2->Add(sizer21, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, FromDIP(14)); + sizer2->AddSpacer(FromDIP(6)); + wxSizer* sizer22 = new wxBoxSizer(wxHORIZONTAL); + sizer22->Add(img1, 0, wxALIGN_CENTRE); + sizer22->Add(txt1, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer22->Add(val1, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer2->Add(sizer22, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, FromDIP(14)); + box->SetSizer(sizer2); sizer->Add(box, 0, wxTOP | wxBOTTOM | wxLEFT | wxRIGHT | wxEXPAND, FromDIP(14)); SetSizer(sizer); @@ -1188,11 +1245,11 @@ public: Fit(); Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) { - wxPaintDC dc(this); - dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0); - }); + wxPaintDC dc(this); + dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0); + }); SetBackgroundColour(*wxWHITE); wxGetApp().UpdateDarkUIWin(this); @@ -1200,35 +1257,34 @@ public: static void SetAMSCount(int index, int ams4, int ams1) { - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; preset_bundle.extruder_ams_counts.resize(2); - auto &ams_map = preset_bundle.extruder_ams_counts[index]; - ams_map[4] = ams4; - ams_map[1] = ams1; + auto& ams_map = preset_bundle.extruder_ams_counts[index]; + ams_map[4] = ams4; + ams_map[1] = ams1; - std::vector extruder_ams_count = save_extruder_ams_count_to_string(preset_bundle.extruder_ams_counts); - std::string extruder_ams_count_str = boost::algorithm::join(extruder_ams_count, ","); + std::vector extruder_ams_count = save_extruder_ams_count_to_string(preset_bundle.extruder_ams_counts); + std::string extruder_ams_count_str = boost::algorithm::join(extruder_ams_count, ","); wxGetApp().app_config->set("presets", "extruder_ams_count", extruder_ams_count_str); wxGetApp().plater()->update(); // update slice status } - static void GetAMSCount(int index, int & ams4, int & ams1) + static void GetAMSCount(int index, int& ams4, int& ams1) { - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; if (preset_bundle.extruder_ams_counts.empty()) { ams4 = 0; ams1 = 0; - } - else { + } else { assert(preset_bundle.extruder_ams_counts.size() == 2); ams4 = preset_bundle.extruder_ams_counts[index][4]; ams1 = preset_bundle.extruder_ams_counts[index][1]; } } - static void UpdateAMSCount(int index, ExtruderGroup *extruder) + static void UpdateAMSCount(int index, ExtruderGroup* extruder) { - std::vector> &ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; + std::vector>& ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; ams_counts.resize(2); std::map& ams_map = ams_counts[index]; if (ams_map.find(4) == ams_map.end()) { @@ -1242,8 +1298,7 @@ public: } }; -ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) - : StaticGroup(parent, wxID_ANY, wxString()) +ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) : StaticGroup(parent, wxID_ANY, wxString()) { SetFont(Label::Body_10); SetForegroundColour(wxColour("#CECECE")); @@ -1256,20 +1311,22 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title hover_label = new HoverLabel(this, title); // Nozzle - wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter")); + wxStaticText* label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter")); label_diameter->SetFont(Label::Body_14); label_diameter->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - if (index >= 0) label_diameter->SetMinSize({FromDIP(80), -1}); - auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); - this->combo_diameter = combo_diameter; - wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow")); + if (index >= 0) + label_diameter->SetMinSize({FromDIP(80), -1}); + auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + this->combo_diameter = combo_diameter; + wxStaticText* label_flow = new wxStaticText(this, wxID_ANY, _L("Flow")); label_flow->SetFont(Label::Body_14); label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1}); + if (index >= 0) + label_flow->SetMinSize({FromDIP(80), -1}); auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); combo_flow->GetDropDown().SetUseContentWidth(true); - combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent &evt) { - auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent& evt) { + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))); printer_tab->set_extruder_volume_type(index, volume_type); auto plater = GUI::wxGetApp().plater(); @@ -1284,27 +1341,36 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title this->combo_flow = combo_flow; // AMS - wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS")); + wxStaticText* label_ams = new wxStaticText(this, wxID_ANY, _L("AMS")); label_ams->SetFont(Label::Body_14); label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - //label_ams->SetMinSize({FromDIP(70), -1}); + // label_ams->SetMinSize({FromDIP(70), -1}); if (index >= 0) { btn_edit = new ScalableButton(this, wxID_ANY, "dot"); btn_edit->SetBackgroundColour(extruder_group_chip_bg()); btn_edit->Hide(); - btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { - PopupWindow *window = new AMSCountPopupWindow(this, index); - auto size = GetSize(); - auto pos = ClientToScreen({0, size.y + 12}); + btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto& evt) { + PopupWindow* window = new AMSCountPopupWindow(this, index); + auto size = GetSize(); + auto pos = ClientToScreen({0, size.y + 12}); size.SetWidth(size.GetWidth() + FromDIP(10)); window->Position(pos, {0, 0}); window->Popup(); }); - auto hovered = std::make_shared(); - for (wxWindow *w : std::initializer_list{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) { - w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); }); - w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } }); + auto hovered = std::make_shared(); + for (wxWindow* w : + std::initializer_list{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) { + w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent& evt) { + *hovered = w; + btn_edit->SetBitmap_("edit"); + }); + w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent& evt) { + if (*hovered == w) { + btn_edit->SetBitmap_("dot"); + *hovered = nullptr; + } + }); } } @@ -1326,42 +1392,44 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2)); hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER); - btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); + btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, + false, 14); btn_up->SetBackgroundColour(*wxWHITE); - btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto& evt) { if (page_cur > 0) --page_cur; update_ams(); }); btn_up->Hide(); - btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); + btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, false, 14); btn_down->SetBackgroundColour(*wxWHITE); - btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto& evt) { if (page_cur + 1 < page_num) ++page_cur; update_ams(); }); btn_down->Hide(); - wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* hsizer_diameter = new wxBoxSizer(wxHORIZONTAL); hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER); hsizer_diameter->Add(combo_diameter, 1, wxEXPAND); - wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL); hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER); hsizer_nozzle->Add(combo_flow, 1, wxEXPAND); if (index < 0) { label_ams->Hide(); ams_not_installed_msg->Hide(); - wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); - wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); - hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8)); + wxStaticBoxSizer* vsizer = new wxStaticBoxSizer(this, wxVERTICAL); + wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); + hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8)); hsizer->AddSpacer(FromDIP(2)); // Avoid badge vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2)); this->sizer = vsizer; } else { - wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); + wxStaticBoxSizer* vsizer = new wxStaticBoxSizer(this, wxVERTICAL); vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2)); vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2)); @@ -1376,7 +1444,8 @@ void ExtruderGroup::update_ams() static AMSinfo info4; static AMSinfo info1; if (info4.cans.empty()) { - for (size_t i = 0; i < 4; ++i) info4.cans.push_back({}); + for (size_t i = 0; i < 4; ++i) + info4.cans.push_back({}); info1.ams_type = AMSModel::N3S_AMS; info1.cans.push_back({}); } @@ -1427,7 +1496,7 @@ void ExtruderGroup::update_ams() hsizer_ams->AddStretchSpacer(1); if (btn_up->IsShown() && btn_down->IsShown()) { auto vsizer_btn = new wxBoxSizer(wxVERTICAL); - auto size = btn_up->GetSize(); + auto size = btn_up->GetSize(); vsizer_btn->Add(btn_up, 0); vsizer_btn->Add(btn_down, 0); hsizer_ams->Add(vsizer_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(2)); @@ -1441,16 +1510,16 @@ void ExtruderGroup::update_ams() sizer->Layout(); } -void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector const &ams1) +void ExtruderGroup::sync_ams(MachineObject const* obj, std::vector const& ams4, std::vector const& ams1) { - if (ams_4.empty() && ams4.empty() - && ams_1.empty() && ams1.empty()) + if (ams_4.empty() && ams4.empty() && ams_1.empty() && ams1.empty()) return; - auto sync = [obj](std::vector &infos, std::vector const &ams) -> bool { + auto sync = [obj](std::vector& infos, std::vector const& ams) -> bool { std::vector infos2; for (auto a : ams) { AMSinfo ams_info; - ams_info.parse_ams_info(const_cast(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity); + ams_info.parse_ams_info(const_cast(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), + obj->is_support_ams_humidity); infos2.push_back(ams_info); } if (infos == infos2) @@ -1474,12 +1543,14 @@ bool Sidebar::priv::switch_diameter(bool single) if (single) { diameter = single_extruder->combo_diameter->GetValue(); } else { - auto diameter_left = left_extruder->combo_diameter->GetValue(); + auto diameter_left = left_extruder->combo_diameter->GetValue(); auto diameter_right = right_extruder->combo_diameter->GetValue(); if (diameter_left != diameter_right) { std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle); - auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase)); - auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase)); + auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, + ToolHeadNameCase::SentenceCase)); + auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, + ToolHeadNameCase::SentenceCase)); MessageDialog dlg(this->plater, _L("The software does not support using different diameter of nozzles for one print. " "If the left and right nozzles are inconsistent, we can only proceed with single-head printing. " @@ -1494,15 +1565,14 @@ bool Sidebar::priv::switch_diameter(bool single) diameter = diameter_right; else return false; - } - else { + } else { diameter = diameter_left; } } - + // ORCA: Check if the selected diameter matches the current nozzle diameter in the config Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); - auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); + auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); if (nozzle_diameter && nozzle_diameter->size() > 0) { auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]); // If the selected diameter is the same as current nozzle, don't switch profiles @@ -1510,8 +1580,8 @@ bool Sidebar::priv::switch_diameter(bool single) return true; } } - - auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter.ToStdString()); + + auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter.ToStdString()); if (preset == nullptr) { // ORCA add a text. this appears when user tries to change nozzle value but config doesnt have a inherited or compatible preset MessageDialog dlg(this->plater, _L("Configuration incompatible"), _L("Warning"), wxICON_WARNING | wxOK); @@ -1524,9 +1594,7 @@ bool Sidebar::priv::switch_diameter(bool single) static bool is_skip_high_flow_printer(const std::string& printer) { - static const std::set invalidate_list = { - "Bambu Lab X1E" - }; + static const std::set invalidate_list = {"Bambu Lab X1E"}; return invalidate_list.count(printer); }; @@ -1551,7 +1619,8 @@ static std::string serialize_nozzle_config(const std::map 0) oss << ";"; + if (i > 0) + oss << ";"; oss << std::fixed << std::setprecision(1) << deputy_nozzles[i].GetNozzleDiameter() << "," << static_cast(deputy_nozzles[i].GetNozzleFlowType()); } @@ -1559,7 +1628,8 @@ static std::string serialize_nozzle_config(const std::map 0) oss << ";"; + if (i > 0) + oss << ";"; oss << std::fixed << std::setprecision(1) << main_nozzles[i].GetNozzleDiameter() << "," << static_cast(main_nozzles[i].GetNozzleFlowType()); } @@ -1567,10 +1637,11 @@ static std::string serialize_nozzle_config(const std::map> deserialize_nozzle_config(const std::string &config_str) +static std::map> deserialize_nozzle_config(const std::string& config_str) { std::map> nozzle_cfg_map; - if (config_str.empty()) return nozzle_cfg_map; + if (config_str.empty()) + return nozzle_cfg_map; std::vector extruder_parts; boost::split(extruder_parts, config_str, boost::is_any_of("|")); @@ -1579,12 +1650,12 @@ static std::map> deserialize_nozzle_config(const std std::vector nozzles; std::vector parts; boost::split(parts, part_str, boost::is_any_of(";")); - for (const auto &part : parts) { + for (const auto& part : parts) { std::vector values; boost::split(values, part, boost::is_any_of(",")); if (values.size() == 2) { DevNozzle nozzle; - nozzle.m_diameter = std::stof(values[0]); + nozzle.m_diameter = std::stof(values[0]); nozzle.m_nozzle_flow = static_cast(std::stoi(values[1])); nozzles.push_back(nozzle); } @@ -1593,39 +1664,42 @@ static std::map> deserialize_nozzle_config(const std }; if (extruder_parts.size() != 2) { - auto nozzles = get_nozzles_from_string(config_str); - nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; - nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = { DevNozzle() }; + auto nozzles = get_nozzles_from_string(config_str); + nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; + nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = {DevNozzle()}; return nozzle_cfg_map; } if (!extruder_parts[0].empty()) { - auto nozzles = get_nozzles_from_string(extruder_parts[0]); + auto nozzles = get_nozzles_from_string(extruder_parts[0]); nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = nozzles; } if (!extruder_parts[1].empty()) { - auto nozzles = get_nozzles_from_string(extruder_parts[1]); + auto nozzles = get_nozzles_from_string(extruder_parts[1]); nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; } return nozzle_cfg_map; } -static bool is_same_nozzle_config(const std::map> &config1, const std::map> &config2) +static bool is_same_nozzle_config(const std::map>& config1, const std::map>& config2) { - if (config1.size() != config2.size()) return false; + if (config1.size() != config2.size()) + return false; for (const auto& [eid, nozzles1] : config1) { auto it = config2.find(eid); - if (it == config2.end()) return false; + if (it == config2.end()) + return false; - const auto &nozzles2 = it->second; - if (nozzles1.size() != nozzles2.size()) return false; + const auto& nozzles2 = it->second; + if (nozzles1.size() != nozzles2.size()) + return false; auto sorted_nozzles1 = nozzles1; auto sorted_nozzles2 = nozzles2; - auto compare_nozzle = [](const DevNozzle &a, const DevNozzle &b) { + auto compare_nozzle = [](const DevNozzle& a, const DevNozzle& b) { float dia_a = a.GetNozzleDiameter(); float dia_b = b.GetNozzleDiameter(); if (std::abs(dia_a - dia_b) > EPSILON) { @@ -1648,19 +1722,22 @@ static bool is_same_nozzle_config(const std::map> &c return true; } -static std::string serialize_nozzle_option(const NozzleOption& option) { +static std::string serialize_nozzle_option(const NozzleOption& option) +{ std::ostringstream oss; oss << option.diameter << "|"; bool first = true; for (const auto& pair : option.extruder_nozzle_stats) { - if (!first) oss << ";"; + if (!first) + oss << ";"; first = false; oss << pair.first << ":"; bool first_stat = true; for (const auto& stat_pair : pair.second) { - if (!first_stat) oss << ","; + if (!first_stat) + oss << ","; first_stat = false; oss << static_cast(stat_pair.first) << "#" << stat_pair.second; } @@ -1668,12 +1745,15 @@ static std::string serialize_nozzle_option(const NozzleOption& option) { return oss.str(); } -static std::optional deserialize_nozzle_option(const std::string& option_str) { - if (option_str.empty()) return std::nullopt; +static std::optional deserialize_nozzle_option(const std::string& option_str) +{ + if (option_str.empty()) + return std::nullopt; std::vector parts; boost::split(parts, option_str, boost::is_any_of("|")); - if (parts.size() != 2) return std::nullopt; + if (parts.size() != 2) + return std::nullopt; NozzleOption option; option.diameter = parts[0]; @@ -1682,11 +1762,13 @@ static std::optional deserialize_nozzle_option(const std::string& boost::split(extruder_parts, parts[1], boost::is_any_of(";")); for (const auto& extruder_part : extruder_parts) { - if (extruder_part.empty()) continue; + if (extruder_part.empty()) + continue; std::vector extruder_data; boost::split(extruder_data, extruder_part, boost::is_any_of(":")); - if (extruder_data.size() != 2) continue; + if (extruder_data.size() != 2) + continue; int extruder_id = std::stoi(extruder_data[0]); std::unordered_map stats; @@ -1699,8 +1781,8 @@ static std::optional deserialize_nozzle_option(const std::string& boost::split(kv, stat_part, boost::is_any_of("#")); if (kv.size() == 2) { NozzleVolumeType type = static_cast(std::stoi(kv[0])); - int count = std::stoi(kv[1]); - stats[type] = count; + int count = std::stoi(kv[1]); + stats[type] = count; } } @@ -1710,16 +1792,21 @@ static std::optional deserialize_nozzle_option(const std::string& return option; } -std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual) +std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj, + int extruder_count, + bool support_multi_nozzle, + bool is_manual) { if (extruder_count < 2 || !support_multi_nozzle) { return std::nullopt; } - if (!obj || !obj->GetNozzleSystem()) return std::nullopt; + if (!obj || !obj->GetNozzleSystem()) + return std::nullopt; auto nozzle_system = obj->GetNozzleSystem(); - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) return std::nullopt; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return std::nullopt; std::string curr_dev_id = obj->get_dev_id(); std::map> curr_nozzle_cfg; @@ -1733,8 +1820,8 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj curr_nozzle_cfg[MAIN_EXTRUDER_ID].emplace_back(rack_nozzle.second); } - AppConfig *app_config = wxGetApp().app_config; - std::string saved_dev_id = app_config->get("sync_extruder", "dev_id"); + AppConfig* app_config = wxGetApp().app_config; + std::string saved_dev_id = app_config->get("sync_extruder", "dev_id"); std::string saved_nozzle_config_str = app_config->get("sync_extruder", "nozzle_config"); std::string saved_nozzle_option_str = app_config->get("sync_extruder", "nozzle_option"); std::optional nozzle_option; @@ -1746,20 +1833,20 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj if (!saved_dev_id.empty() && !saved_nozzle_config_str.empty() && !saved_nozzle_option_str.empty() && saved_dev_id == curr_dev_id) { auto saved_nozzle_config = deserialize_nozzle_config(saved_nozzle_config_str); if (is_same_nozzle_config(saved_nozzle_config, curr_nozzle_cfg)) { - nozzle_option = deserialize_nozzle_option(saved_nozzle_option_str); + nozzle_option = deserialize_nozzle_option(saved_nozzle_option_str); can_reuse_saved_option = nozzle_option.has_value(); } if (can_reuse_saved_option) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Reusing saved nozzle option for dev_id: " << curr_dev_id; - auto &project_config = preset_bundle->project_config; - ConfigOptionEnumsGeneric *nozzle_volume_type_opt = project_config.option("nozzle_volume_type"); + auto& project_config = preset_bundle->project_config; + ConfigOptionEnumsGeneric* nozzle_volume_type_opt = project_config.option("nozzle_volume_type"); // Write to preset bundle config for (int extruder_id = 0; extruder_id < extruder_count; ++extruder_id) { NozzleVolumeType volume_type; - int nozzle_count; - bool clear_all = true; + int nozzle_count; + bool clear_all = true; if (!nozzle_option->extruder_nozzle_stats.count(extruder_id)) { nozzle_count = 0; @@ -1776,7 +1863,7 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj clear_all = false; } } else { - for (auto &stat : nozzle_option->extruder_nozzle_stats[extruder_id]) { + for (auto& stat : nozzle_option->extruder_nozzle_stats[extruder_id]) { volume_type = stat.first; nozzle_count = stat.second; setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, nozzle_count, clear_all); @@ -1788,7 +1875,8 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj // a manual flow switch, and refresh the sidebar badges. setNozzleStatsFromMachine(true); if (nozzle_volume_type_opt) { - for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volume_type_opt->values.size(); ++extruder_id) + for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volume_type_opt->values.size(); + ++extruder_id) updateNozzleCountDisplay(preset_bundle, extruder_id, NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id])); } } @@ -1842,7 +1930,7 @@ void Sidebar::priv::show_fila_switch_msg(bool ready) long style = ready ? (wxICON_INFORMATION | wxOK) : (wxICON_WARNING | wxOK); // Orca: drop the vendor "Learn more" tracking link; there is no Orca help page for the switch yet. - MessageDialog dlg(static_cast(wxGetApp().mainframe), msg, _L("Tips"), style); + MessageDialog dlg(static_cast(wxGetApp().mainframe), msg, _L("Tips"), style); dlg.CenterOnParent(); dlg.ShowModal(); } @@ -1892,12 +1980,12 @@ void Sidebar::priv::update_extruder_separator_icon(bool show, bool ready) // left_extruder->GetPosition() and the icon position live in the same coordinate space. wxPoint left_box_pos = left_extruder->GetPosition(); wxPoint ams_local_pos = left_extruder->hsizer_ams->GetPosition(); - wxSize left_size = left_extruder->sizer->GetSize(); - wxSize ams_size = left_extruder->hsizer_ams->GetSize(); - wxSize icon_size = extruder_separator_icon->GetSize(); - int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); - int center_x = left_size.GetWidth() + FromDIP(6); - int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2; + wxSize left_size = left_extruder->sizer->GetSize(); + wxSize ams_size = left_extruder->hsizer_ams->GetSize(); + wxSize icon_size = extruder_separator_icon->GetSize(); + int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); + int center_x = left_size.GetWidth() + FromDIP(6); + int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2; center_x -= icon_size.GetWidth() / 2; center_y -= icon_size.GetHeight() / 2; extruder_separator_icon->SetPosition(wxPoint(center_x, center_y)); @@ -1916,19 +2004,19 @@ void Sidebar::priv::update_extruder_separator_icon(bool show, bool ready) m_panel_printer_content->Refresh(); } -bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_manual) +bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_manual) { - MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); - auto printer_name = plater->get_selected_printer_name_in_combox(); + MachineObject* obj = wxGetApp().getDeviceManager()->get_selected_machine(); + auto printer_name = plater->get_selected_printer_name_in_combox(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " begin sync_extruder_list"; if (obj == nullptr) { plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); return false; } - //if (obj->get_extder_system()->extders.size() != 2) {//wxString(obj->get_preset_printer_model_name(machine_print_name)) - // plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::INCONSISTENT, _L("Sync printer information")); - // return false; - //} + // if (obj->get_extder_system()->extders.size() != 2) {//wxString(obj->get_preset_printer_model_name(machine_print_name)) + // plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::INCONSISTENT, _L("Sync printer + // information")); return false; + // } if (!plater->check_printer_initialized(obj)) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " check_printer_initialized fail"; @@ -1936,16 +2024,18 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man } std::string machine_print_name = obj->get_show_printer_type(); - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); - Preset* machine_preset = get_printer_preset(obj); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); + Preset* machine_preset = get_printer_preset(obj); if (!machine_preset) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; return false; } if (machine_print_name != target_model_id) { - MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" - "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); + MessageDialog dlg(this->plater, + _L("The currently selected machine preset is inconsistent with the connected printer type.\n" + "Are you sure to continue syncing?"), + _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); if (dlg.ShowModal() == wxID_NO) { return false; } @@ -1954,16 +2044,16 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man return false; this->plater->update_objects_position_when_select_preset([&obj, machine_preset]() { - Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); + Tab* printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); printer_tab->select_preset(machine_preset->name); }); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list"; - const Preset &cur_preset = preset_bundle->printers.get_selected_preset(); - int extruder_nums = preset_bundle->get_printer_extruder_count(); + const Preset& cur_preset = preset_bundle->printers.get_selected_preset(); + int extruder_nums = preset_bundle->get_printer_extruder_count(); std::vector extruder_map(extruder_nums); std::iota(extruder_map.begin(), extruder_map.end(), 0); - const ConfigOptionInts *physical_extruder_map = cur_preset.config.option("physical_extruder_map"); + const ConfigOptionInts* physical_extruder_map = cur_preset.config.option("physical_extruder_map"); if (physical_extruder_map != nullptr) { assert(physical_extruder_map->values.size() == extruder_nums); extruder_map = physical_extruder_map->values; @@ -1975,11 +2065,12 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man // value > 1 (nil-guarded, matching the manual/ToolOrdering gates), which no shipping single-nozzle or // dual-extruder profile sets. When the machine is multi-nozzle but no option resolves (e.g. user cancelled), // abort the sync. - const ConfigOptionIntsNullable *extruder_max_nozzle_count = cur_preset.config.option("extruder_max_nozzle_count"); + const ConfigOptionIntsNullable* extruder_max_nozzle_count = cur_preset.config.option( + "extruder_max_nozzle_count"); bool support_multi_nozzle = extruder_max_nozzle_count != nullptr && std::any_of(extruder_max_nozzle_count->values.begin(), extruder_max_nozzle_count->values.end(), [](int val) { return val > 1 && val != ConfigOptionIntsNullable::nil_value(); }); - auto nozzle_option = get_nozzle_options(obj, extruder_nums, support_multi_nozzle, is_manual); + auto nozzle_option = get_nozzle_options(obj, extruder_nums, support_multi_nozzle, is_manual); if (!nozzle_option && support_multi_nozzle) return false; @@ -1987,12 +2078,13 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man nozzle_diameters.resize(extruder_nums); std::vector target_types(extruder_nums, NozzleVolumeType::nvtStandard); for (size_t index = 0; index < extruder_nums; ++index) { - int extruder_id = extruder_map[index]; - nozzle_diameters[extruder_id] = nozzle_option ? atof(nozzle_option->diameter.c_str()) : obj->GetExtderSystem()->GetNozzleDiameter(index); - NozzleVolumeType target_type = NozzleVolumeType::nvtStandard; + int extruder_id = extruder_map[index]; + nozzle_diameters[extruder_id] = nozzle_option ? atof(nozzle_option->diameter.c_str()) : + obj->GetExtderSystem()->GetNozzleDiameter(index); + NozzleVolumeType target_type = NozzleVolumeType::nvtStandard; std::optional select_type; if (nozzle_option && nozzle_option->extruder_nozzle_stats.count(index)) { - const auto &stats = nozzle_option->extruder_nozzle_stats[index]; + const auto& stats = nozzle_option->extruder_nozzle_stats[index]; if (stats.size() > 1) // The extruder holds nozzles of several flow types: select the mixed-flow mode. select_type = NozzleVolumeType::nvtHybrid; @@ -2001,7 +2093,8 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man } if (obj->is_nozzle_flow_type_supported()) { if (obj->GetExtderSystem()->GetNozzleFlowType(index) == NozzleFlowType::NONE_FLOWTYPE) { - MessageDialog dlg(this->plater, _L("There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing."), + MessageDialog dlg(this->plater, + _L("There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing."), _L("Sync extruder infomation"), wxICON_WARNING | wxOK); dlg.ShowModal(); continue; @@ -2029,23 +2122,29 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man auto switcher_pos = ams.second->GetSwitcherPos(); if (!switcher_pos) continue; - int switcher_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) - : static_cast(switcher_pos.value()); + int switcher_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) : + static_cast(switcher_pos.value()); if (extruder_id != switcher_id) continue; } const bool is_n3s = ams.second->GetAmsType() == DevAms::N3S; // Main (first) extruder is id 0, deputy is id 1. if (extruder_id == 0) { - if (is_n3s) ++main_1; else ++main_4; + if (is_n3s) + ++main_1; + else + ++main_4; } else if (extruder_id == 1) { - if (is_n3s) ++deputy_1; else ++deputy_4; + if (is_n3s) + ++deputy_1; + else + ++deputy_4; } } } only_external_material = !obj->GetFilaSystem()->HasAms(); - int main_index = obj->is_main_extruder_on_left() ? 0 : 1; - int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0; + int main_index = obj->is_main_extruder_on_left() ? 0 : 1; + int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0; if (extruder_nums > 1) { int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); @@ -2070,7 +2169,7 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man } // set nozzle volume type after switching prset, so this value can override the old value stored in conf - auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); for (size_t idx = 0; idx < target_types.size(); ++idx) { printer_tab->set_extruder_volume_type(idx, target_types[idx]); } @@ -2085,17 +2184,17 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man // Copy the live switch state into the project so slicing and the send dialog honor it. Both // resolve to false unless the switch is installed and calibrated, so nothing changes without one. - auto &project_config = wxGetApp().preset_bundle->project_config; - if (auto *dynamic_filament = project_config.opt("enable_filament_dynamic_map")) + auto& project_config = wxGetApp().preset_bundle->project_config; + if (auto* dynamic_filament = project_config.opt("enable_filament_dynamic_map")) dynamic_filament->value = is_fila_switch_ready(); - if (auto *has_switcher = project_config.opt("has_filament_switcher")) + if (auto* has_switcher = project_config.opt("has_filament_switcher")) has_switcher->value = is_fila_switch_ready(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " finish sync_extruder_list"; return true; } -void Sidebar::priv::update_sync_status(const MachineObject *obj) +void Sidebar::priv::update_sync_status(const MachineObject* obj) { StateColor not_synced_colour(std::pair(wxColour("#009688"), StateColor::Normal)); auto clear_all_sync_status = [this, ¬_synced_colour]() { @@ -2109,8 +2208,8 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) single_extruder->ShowBadge(false); single_extruder->sync_ams(nullptr, {}, {}); update_extruder_separator_icon(false, false); - //btn_sync_printer->SetBorderColor(not_synced_colour); - //btn_sync_printer->SetIcon("printer_sync"); + // btn_sync_printer->SetBorderColor(not_synced_colour); + // btn_sync_printer->SetIcon("printer_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_not"); }; @@ -2129,7 +2228,7 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) fila_switch_warning_shown = false; } - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (!preset_bundle) { clear_all_sync_status(); return; @@ -2137,7 +2236,7 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) bool printer_synced = false; // 1. update printer status - const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + const Preset& cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type()) { panel_printer_preset->ShowBadge(true); printer_synced = true; @@ -2153,25 +2252,23 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) struct ExtruderInfo { float diameter{0.4}; - //int nozzle_volue_type{0}; - int ams_4{0}; - int ams_1{0}; - std::vector ams_v4; - std::vector ams_v1; + // int nozzle_volue_type{0}; + int ams_4{0}; + int ams_1{0}; + std::vector ams_v4; + std::vector ams_v1; - bool operator==(const ExtruderInfo &other) const + bool operator==(const ExtruderInfo& other) const { - return abs(diameter - other.diameter) < EPSILON - && /*nozzle_volue_type == other.nozzle_volue_type - &&*/ ams_4 == other.ams_4 - && ams_1 == other.ams_1; + return abs(diameter - other.diameter) < EPSILON && /*nozzle_volue_type == other.nozzle_volue_type + &&*/ ams_4 == other.ams_4 && ams_1 == other.ams_1; } }; - auto is_same_nozzle_info = [obj](const ExtruderInfo &left, const ExtruderInfo &right) { + auto is_same_nozzle_info = [obj](const ExtruderInfo& left, const ExtruderInfo& right) { bool is_same_nozzle_type = true; if (obj->is_nozzle_flow_type_supported()) - is_same_nozzle_type = true;//left.nozzle_volue_type == right.nozzle_volue_type; // TODO: Orca hack + is_same_nozzle_type = true; // left.nozzle_volue_type == right.nozzle_volue_type; // TODO: Orca hack return abs(left.diameter - right.diameter) < EPSILON && is_same_nozzle_type; }; @@ -2196,10 +2293,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) } std::vector extruder_infos(extruder_nums); - std::vector nozzle_volume_types = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; - //for (size_t i = 0; i < nozzle_volume_types.size(); ++i) { - // extruder_infos[i].nozzle_volue_type = nozzle_volume_types[i]; - //} + std::vector nozzle_volume_types = + wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; + // for (size_t i = 0; i < nozzle_volume_types.size(); ++i) { + // extruder_infos[i].nozzle_volue_type = nozzle_volume_types[i]; + // } std::vector> extruder_ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; if (extruder_ams_counts.size() >= extruder_nums) { @@ -2217,12 +2315,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) double value = 0.0; single_extruder->diameter.ToDouble(&value); extruder_infos[0].diameter = float(value); - } - else if(extruder_nums == 2){ + } else if (extruder_nums == 2) { double value = 0.0; left_extruder->diameter.ToDouble(&value); extruder_infos[0].diameter = float(value); - + value = 0.0; right_extruder->diameter.ToDouble(&value); extruder_infos[1].diameter = float(value); @@ -2231,11 +2328,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) std::vector machine_extruder_infos(obj->GetExtderSystem()->GetTotalExtderCount()); const auto& extruders = obj->GetExtderSystem()->GetExtruders(); - for (const DevExtder &extruder : extruders) { - //machine_extruder_infos[extruder.GetExtId()].nozzle_volue_type = int(extruder.GetNozzleFlowType()) - 1; - machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter(); + for (const DevExtder& extruder : extruders) { + // machine_extruder_infos[extruder.GetExtId()].nozzle_volue_type = int(extruder.GetNozzleFlowType()) - 1; + machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter(); } - for (auto &item : obj->GetFilaSystem()->GetAmsList()) { + for (auto& item : obj->GetFilaSystem()->GetAmsList()) { int extruder_id; // With the switch ready every AMS feeds both extruders, so attribute each to the extruder its // input track feeds. Without a switch the binding is a single extruder @@ -2245,10 +2342,10 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) auto switcher_pos = item.second->GetSwitcherPos(); if (!switcher_pos) continue; - extruder_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) - : static_cast(switcher_pos.value()); + extruder_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) : + static_cast(switcher_pos.value()); } else { - const auto &uniq_extruder_id = item.second->GetUniqueBindedExtruderId(); + const auto& uniq_extruder_id = item.second->GetUniqueBindedExtruderId(); if (!uniq_extruder_id) continue; extruder_id = uniq_extruder_id.value(); @@ -2257,8 +2354,7 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) if (extruder_id >= machine_extruder_infos.size()) continue; - if (item.second->GetAmsType() == DevAms::N3S) - { // N3S + if (item.second->GetAmsType() == DevAms::N3S) { // N3S machine_extruder_infos[extruder_id].ams_1++; machine_extruder_infos[extruder_id].ams_v1.push_back(item.second); } else { @@ -2276,20 +2372,17 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) panel_nozzle_dia->ShowBadge(true); // ORCA add support for nozzle sync single_extruder->sync_ams(obj, machine_extruder_infos[0].ams_v4, machine_extruder_infos[0].ams_v1); extruder_synced[0] = true; - } - else { + } else { single_extruder->ShowBadge(false); panel_nozzle_dia->ShowBadge(false); // ORCA add support for nozzle sync single_extruder->sync_ams(obj, {}, {}); } - } - else if (extruder_nums == 2) { + } else if (extruder_nums == 2) { if (extruder_infos[0] == machine_extruder_infos[0]) { left_extruder->ShowBadge(true); left_extruder->sync_ams(obj, machine_extruder_infos[0].ams_v4, machine_extruder_infos[0].ams_v1); extruder_synced[0] = true; - } - else { + } else { left_extruder->ShowBadge(false); left_extruder->sync_ams(obj, {}, {}); } @@ -2298,8 +2391,7 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) right_extruder->ShowBadge(true); right_extruder->sync_ams(obj, machine_extruder_infos[1].ams_v4, machine_extruder_infos[1].ams_v1); extruder_synced[1] = true; - } - else { + } else { right_extruder->ShowBadge(false); right_extruder->sync_ams(obj, {}, {}); } @@ -2308,13 +2400,12 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) StateColor synced_colour(std::pair(wxColour("#CECECE"), StateColor::Normal)); bool all_extruder_synced = std::all_of(extruder_synced.begin(), extruder_synced.end(), [](bool value) { return value; }); if (printer_synced && all_extruder_synced) { - // btn_sync_printer->SetBorderColor(synced_colour); - // btn_sync_printer->SetIcon("ams_nozzle_sync"); + // btn_sync_printer->SetBorderColor(synced_colour); + // btn_sync_printer->SetIcon("ams_nozzle_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_ok"); - } - else { - // btn_sync_printer->SetBorderColor(not_synced_colour); - // btn_sync_printer->SetIcon("printer_sync"); + } else { + // btn_sync_printer->SetBorderColor(not_synced_colour); + // btn_sync_printer->SetIcon("printer_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_not"); } @@ -2327,45 +2418,43 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) last_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list; const auto print_tech = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology(); if (print_tech == ptFFF && !is_same_ams_list) { - for (PlaterPresetComboBox *cb : combos_filament) + for (PlaterPresetComboBox* cb : combos_filament) cb->update(); } } - } +} -void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent &e) - { - if (m_last_slice_state != p->plater->is_background_process_slicing()) { - m_last_slice_state = p->plater->is_background_process_slicing(); - //btn_sync->Enable(!m_last_slice_state); - p->m_printer_bbl_sync->Enable(!m_last_slice_state); - ams_btn->Enable(!m_last_slice_state); - Refresh(); - } - } - -Sidebar::Sidebar(Plater *parent) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(39 * wxGetApp().em_unit(), -1)), p(new priv(parent)) +void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent& e) { - Choice::register_dynamic_list("support_filament", &dynamic_filament_list); - Choice::register_dynamic_list("support_interface_filament", &dynamic_filament_list); + if (m_last_slice_state != p->plater->is_background_process_slicing()) { + m_last_slice_state = p->plater->is_background_process_slicing(); + // btn_sync->Enable(!m_last_slice_state); + p->m_printer_bbl_sync->Enable(!m_last_slice_state); + ams_btn->Enable(!m_last_slice_state); + Refresh(); + } +} + +Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(39 * wxGetApp().em_unit(), -1)), p(new priv(parent)) +{ + Choice::register_dynamic_list("support_filament", &dynamic_physical_filament_list); + Choice::register_dynamic_list("support_interface_filament", &dynamic_physical_filament_list); Choice::register_dynamic_list("outer_wall_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("inner_wall_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("sparse_infill_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("internal_solid_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("top_surface_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("bottom_surface_filament_id", &dynamic_filament_list); - Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); + Choice::register_dynamic_list("wipe_tower_filament", &dynamic_physical_filament_list); p->scrolled = new wxPanel(this); // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 // but this cause the bad layout of the sidebar, when all infoboxes appear. // As a result we can see the empty block at the bottom of the sidebar // But if we set this value to 5, layout will be better - //p->scrolled->SetScrollRate(0, 5); + // p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); - SetFont(wxGetApp().normal_font()); #ifndef __APPLE__ #ifdef _WIN32 @@ -2377,14 +2466,14 @@ Sidebar::Sidebar(Plater *parent) #endif int em = wxGetApp().em_unit(); - //BBS refine layout and styles - // Sizer in the scrolled area + // BBS refine layout and styles + // Sizer in the scrolled area auto* scrolled_sizer = m_scrolled_sizer = new wxBoxSizer(wxVERTICAL); p->scrolled->SetSizer(scrolled_sizer); - wxColour title_bg = wxColour(248, 248, 248); - wxColour inactive_text = wxColour(86, 86, 86); - wxColour active_text = wxColour(0, 0, 0); + wxColour title_bg = wxColour(248, 248, 248); + wxColour inactive_text = wxColour(86, 86, 86); + wxColour active_text = wxColour(0, 0, 0); wxColour static_line_col = wxColour(166, 169, 170); #ifdef __WINDOWS__ @@ -2399,18 +2488,18 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_printer_title->SetBackgroundColor(title_bg); p->m_panel_printer_title->SetBackgroundColor2(0xF1F1F1); - p->m_printer_icon = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer"); + p->m_printer_icon = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer"); p->m_text_printer_settings = new Label(p->m_panel_printer_title, _L("Printer"), LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END); p->m_printer_icon->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - //auto wizard_t = new ConfigWizard(wxGetApp().mainframe); - //wizard_t->run(ConfigWizard::RR_USER, ConfigWizard::SP_CUSTOM); - }); + // auto wizard_t = new ConfigWizard(wxGetApp().mainframe); + // wizard_t->run(ConfigWizard::RR_USER, ConfigWizard::SP_CUSTOM); + }); // ORCA use connect button on titlebar p->m_printer_connect = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "monitor_signal_strong"); p->m_printer_connect->SetToolTip(_L("Connection")); - p->m_printer_connect->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + p->m_printer_connect->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { PhysicalPrinterDialog dlg(this->GetParent()); dlg.ShowModal(); }); @@ -2418,25 +2507,25 @@ Sidebar::Sidebar(Plater *parent) // ORCA use sync button on titlebar p->m_printer_bbl_sync = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer_sync_not"); p->m_printer_bbl_sync->SetToolTip(_L("Synchronize nozzle information and the number of AMS")); - p->m_printer_bbl_sync->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { - deal_btn_sync(); - }); + p->m_printer_bbl_sync->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { deal_btn_sync(); }); p->m_printer_setting = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "settings"); - p->m_printer_setting->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + p->m_printer_setting->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { // p->editing_filament = -1; // wxGetApp().params_dialog()->Popup(); // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS); - }); + }); wxBoxSizer* h_sizer_title = new wxBoxSizer(wxHORIZONTAL); h_sizer_title->Add(p->m_printer_icon, 0, wxALIGN_CENTRE | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); h_sizer_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); h_sizer_title->Add(p->m_text_printer_settings, 1, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); - //h_sizer_title->AddStretchSpacer(); - h_sizer_title->Add(p->m_printer_connect , 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks - h_sizer_title->Add(p->m_printer_bbl_sync, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks + // h_sizer_title->AddStretchSpacer(); + h_sizer_title->Add(p->m_printer_connect, 0, wxALIGN_CENTER | wxRIGHT, + FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks + h_sizer_title->Add(p->m_printer_bbl_sync, 0, wxALIGN_CENTER | wxRIGHT, + FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks h_sizer_title->Add(p->m_printer_setting, 0, wxALIGN_CENTER); h_sizer_title->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); h_sizer_title->SetMinSize(-1, 3 * em); @@ -2448,19 +2537,20 @@ Sidebar::Sidebar(Plater *parent) // add printer title scrolled_sizer->Add(p->m_panel_printer_title, 0, wxEXPAND | wxALL, 0); - p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this] (auto & e) { + p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this](auto& e) { if (!p || !p->combo_printer || !p->m_text_printer_settings || !p->m_panel_printer_content || !m_scrolled_sizer) return; // ORCA Show printer name on title when its folded to inform user without expanding it again - bool isShown = p->m_panel_printer_content->IsShown(); - wxString title = _L("Printer") + wxString(!isShown ? "" : (" | " + p->combo_printer->GetValue())); + bool isShown = p->m_panel_printer_content->IsShown(); + wxString title = _L("Printer") + wxString(!isShown ? "" : (" | " + p->combo_printer->GetValue())); p->m_text_printer_settings->SetLabel(title); p->m_panel_printer_content->Show(!isShown); p->m_panel_printer_separator->Show(isShown); m_scrolled_sizer->Layout(); }); // ORCA add bottom border for seperation wile sections folded - p->m_panel_printer_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string + p->m_panel_printer_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, + wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string p->m_panel_printer_separator->SetBackgroundColour("#FFFFFF"); scrolled_sizer->Add(p->m_panel_printer_separator, 0, wxEXPAND); @@ -2469,7 +2559,8 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_printer_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); p->m_panel_printer_content->SetBackgroundColour(wxColour(255, 255, 255)); - struct PanelColors { + struct PanelColors + { wxColour bg_normal = "#FFFFFF"; wxColour bg_focus = "#E5F0EE"; wxColour bd_normal = "#DBDBDB"; @@ -2482,18 +2573,17 @@ Sidebar::Sidebar(Plater *parent) p->panel_printer_preset->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); p->panel_printer_preset->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); - p->panel_printer_preset->Bind(wxEVT_LEFT_DOWN, [this](auto & evt) { - p->combo_printer->wxEvtHandler::ProcessEvent(evt); - }); + p->panel_printer_preset->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { p->combo_printer->wxEvtHandler::ProcessEvent(evt); }); // ORCA Hide Cover automatically if there is not enough space - p->panel_printer_preset->Bind(wxEVT_SIZE, [this](auto & e) { + p->panel_printer_preset->Bind(wxEVT_SIZE, [this](auto& e) { auto current_width = e.GetSize().GetWidth(); auto narrow_width = FromDIP(235); - auto label_width = p->combo_printer->GetTextExtent(p->combo_printer->GetStringSelection()).GetWidth(); - auto min_width = label_width + FromDIP(25 + PRINTER_PANEL_SIZE.GetWidth()); - if(((min_width < narrow_width && min_width > current_width) || (current_width < narrow_width && min_width > narrow_width)) && p->image_printer->IsShown()) + auto label_width = p->combo_printer->GetTextExtent(p->combo_printer->GetStringSelection()).GetWidth(); + auto min_width = label_width + FromDIP(25 + PRINTER_PANEL_SIZE.GetWidth()); + if (((min_width < narrow_width && min_width > current_width) || (current_width < narrow_width && min_width > narrow_width)) && + p->image_printer->IsShown()) p->image_printer->Hide(); - else if((current_width > min_width || !(current_width < narrow_width)) && !p->image_printer->IsShown()) + else if ((current_width > min_width || !(current_width < narrow_width)) && !p->image_printer->IsShown()) p->image_printer->Show(); e.Skip(); }); @@ -2504,16 +2594,18 @@ Sidebar::Sidebar(Plater *parent) p->btn_edit_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "edit"); p->btn_edit_printer->SetToolTip(_L("Click to edit preset")); p->btn_edit_printer->Hide(); // hide for first launch - p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color, printer_preset_hovered](wxCommandEvent){ + p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color, printer_preset_hovered](wxCommandEvent) { p->editing_filament = -1; if (p->combo_printer->switch_to_tab()) p->editing_filament = 0; - // ORCA: FIX crash on wxGTK, directly modifying UI (self->Hide() / parent->Layout()) inside a button event can crash because callbacks are not re-entrant, leaving widgets in an inconsistent state + // ORCA: FIX crash on wxGTK, directly modifying UI (self->Hide() / parent->Layout()) inside a button event can crash because + // callbacks are not re-entrant, leaving widgets in an inconsistent state wxGetApp().CallAfter([this, panel_color, printer_preset_hovered]() { - // ORCA clicking edit button not triggers wxEVT_KILL_FOCUS wxEVT_LEAVE_WINDOW make changes manually to prevent stucked colors when opening printer settings + // ORCA clicking edit button not triggers wxEVT_KILL_FOCUS wxEVT_LEAVE_WINDOW make changes manually to prevent stucked + // colors when opening printer settings if (!p || !p->panel_printer_preset || !p->btn_edit_printer) return; - p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); + p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); printer_preset_hovered->clear(); p->btn_edit_printer->Hide(); p->panel_printer_preset->Layout(); @@ -2521,16 +2613,15 @@ Sidebar::Sidebar(Plater *parent) }); ScalableBitmap bitmap_printer(p->panel_printer_preset, "printer_placeholder", PRINTER_THUMBNAIL_SIZE.GetHeight()); - p->image_printer = new wxStaticBitmap(p->panel_printer_preset, wxID_ANY, bitmap_printer.bmp(), wxDefaultPosition, FromDIP(PRINTER_THUMBNAIL_SIZE), 0); - p->image_printer->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { - p->combo_printer->wxEvtHandler::ProcessEvent(evt); - }); + p->image_printer = new wxStaticBitmap(p->panel_printer_preset, wxID_ANY, bitmap_printer.bmp(), wxDefaultPosition, + FromDIP(PRINTER_THUMBNAIL_SIZE), 0); + p->image_printer->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { p->combo_printer->wxEvtHandler::ProcessEvent(evt); }); p->combo_printer = new PlaterPresetComboBox(p->panel_printer_preset, Preset::TYPE_PRINTER); p->combo_printer->SetBorderWidth(0); p->combo_printer->SetMaxSize(wxSize(-1, FromDIP(30))); // limiting height makes badge visible // ORCA paint whole combobox on focus - auto printer_focus_bg = [this, panel_color](bool focused){ + auto printer_focus_bg = [this, panel_color](bool focused) { auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); p->panel_printer_preset->SetBackgroundColor(bg_color); p->panel_printer_preset->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); @@ -2538,8 +2629,14 @@ Sidebar::Sidebar(Plater *parent) p->image_printer->SetBackgroundColour(bg_color); p->combo_printer->SetBackgroundColour(bg_color); // paints margins instead combo background }; - p->combo_printer->Bind(wxEVT_SET_FOCUS, [this, printer_focus_bg](auto& e) {printer_focus_bg(true ); e.Skip();}); - p->combo_printer->Bind(wxEVT_KILL_FOCUS, [this, printer_focus_bg](auto& e) {printer_focus_bg(false); e.Skip();}); + p->combo_printer->Bind(wxEVT_SET_FOCUS, [this, printer_focus_bg](auto& e) { + printer_focus_bg(true); + e.Skip(); + }); + p->combo_printer->Bind(wxEVT_KILL_FOCUS, [this, printer_focus_bg](auto& e) { + printer_focus_bg(false); + e.Skip(); + }); /* ORCA This part moved to titlebar p->btn_connect_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "monitor_signal_strong"); @@ -2552,14 +2649,15 @@ Sidebar::Sidebar(Plater *parent) }); */ // ORCA use Show/Hide to gain text area instead using blank icon. also manages hover effect for border - for (wxWindow *w : std::initializer_list{p->panel_printer_preset, p->btn_edit_printer, p->image_printer, p->combo_printer}) { - w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent &e) { + for (wxWindow* w : + std::initializer_list{p->panel_printer_preset, p->btn_edit_printer, p->image_printer, p->combo_printer}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent& e) { printer_preset_hovered->insert(w); - if(!p->combo_printer->HasFocus()) + if (!p->combo_printer->HasFocus()) p->panel_printer_preset->SetBorderColor(panel_color.bd_hover); e.Skip(); }); - w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent &e) { + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent& e) { printer_preset_hovered->erase(w); if (printer_preset_hovered->empty() && !p->combo_printer->HasFocus()) p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); @@ -2569,7 +2667,7 @@ Sidebar::Sidebar(Plater *parent) // Perform show/hide in wxEVT_IDLE after enter/leave events have settled. // This prevents the extraneous enter/leave events generated by the // layout change itself from causing a feedback loop. - Bind(wxEVT_IDLE, [this, printer_preset_hovered](wxIdleEvent &e) { + Bind(wxEVT_IDLE, [this, printer_preset_hovered](wxIdleEvent& e) { auto hovered = !printer_preset_hovered->empty(); if (p->btn_edit_printer->IsShown() != hovered) { if (hovered) { @@ -2586,19 +2684,18 @@ Sidebar::Sidebar(Plater *parent) p->panel_nozzle_dia->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal); p->panel_nozzle_dia->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); - p->panel_nozzle_dia->Bind(wxEVT_LEFT_DOWN, [this](auto & evt) { - p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); - }); + p->panel_nozzle_dia->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); }); p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Nozzle"), LB_PROPAGATE_MOUSE_EVENT); p->label_nozzle_title->SetFont(Label::Body_10); - p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, + wxCB_READONLY); p->combo_nozzle_dia->SetBorderWidth(0); p->combo_nozzle_dia->GetDropDown().SetUseContentWidth(true); p->combo_nozzle_dia->SetMinSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // requires a static value in here p->combo_nozzle_dia->SetMaxSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // using -1 with wxEXPAND has issues - p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto &e) { + p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto& e) { auto evt_combo = (*p->single_extruder).combo_diameter; evt_combo->SetSelection(e.GetSelection()); wxCommandEvent evt(wxEVT_COMBOBOX, evt_combo->GetId()); @@ -2608,7 +2705,7 @@ Sidebar::Sidebar(Plater *parent) e.Skip(); }); // ORCA paint whole combobox on focus - auto nozzle_focus_bg = [this, panel_color](bool focused){ + auto nozzle_focus_bg = [this, panel_color](bool focused) { auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); p->panel_nozzle_dia->SetBackgroundColor(bg_color); p->panel_nozzle_dia->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); @@ -2616,24 +2713,32 @@ Sidebar::Sidebar(Plater *parent) p->label_nozzle_type->SetBackgroundColour(bg_color); p->combo_nozzle_dia->SetBackgroundColour(bg_color); // paints margins instead combo background }; - p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [this, nozzle_focus_bg](auto& e) {nozzle_focus_bg(true ); e.Skip();}); - p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [this, nozzle_focus_bg](auto& e) {nozzle_focus_bg(false); e.Skip();}); + p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [this, nozzle_focus_bg](auto& e) { + nozzle_focus_bg(true); + e.Skip(); + }); + p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [this, nozzle_focus_bg](auto& e) { + nozzle_focus_bg(false); + e.Skip(); + }); - p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL); + p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", + LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL); p->label_nozzle_type->SetFont(Label::Body_10); p->label_nozzle_type->SetMinSize(FromDIP(wxSize(56, -1))); p->label_nozzle_type->SetMaxSize(FromDIP(wxSize(56, -1))); // highlight border on hover auto nozzle_dia_hovered = std::make_shared>(); - for (wxWindow *w : std::initializer_list{p->panel_nozzle_dia, p->label_nozzle_title, p->label_nozzle_type, p->combo_nozzle_dia}) { - w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent &e) { + for (wxWindow* w : + std::initializer_list{p->panel_nozzle_dia, p->label_nozzle_title, p->label_nozzle_type, p->combo_nozzle_dia}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent& e) { nozzle_dia_hovered->insert(w); - if(!p->combo_nozzle_dia->HasFocus()) + if (!p->combo_nozzle_dia->HasFocus()) p->panel_nozzle_dia->SetBorderColor(panel_color.bd_hover); e.Skip(); }); - w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent &e) { + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent& e) { nozzle_dia_hovered->erase(w); if (nozzle_dia_hovered->empty() && !p->combo_nozzle_dia->HasFocus()) p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal); @@ -2641,10 +2746,10 @@ Sidebar::Sidebar(Plater *parent) }); } - wxGridSizer *nozzle_dia_sizer = new wxGridSizer(3, 1, FromDIP(2), 0); - nozzle_dia_sizer->Add(p->label_nozzle_title, 1, wxALIGN_CENTER | wxTOP , FromDIP(2)); - nozzle_dia_sizer->Add(p->combo_nozzle_dia , 0, wxALIGN_CENTER); - nozzle_dia_sizer->Add(p->label_nozzle_type , 1, wxALIGN_CENTER | wxBOTTOM, FromDIP(1)); + wxGridSizer* nozzle_dia_sizer = new wxGridSizer(3, 1, FromDIP(2), 0); + nozzle_dia_sizer->Add(p->label_nozzle_title, 1, wxALIGN_CENTER | wxTOP, FromDIP(2)); + nozzle_dia_sizer->Add(p->combo_nozzle_dia, 0, wxALIGN_CENTER); + nozzle_dia_sizer->Add(p->label_nozzle_type, 1, wxALIGN_CENTER | wxBOTTOM, FromDIP(1)); p->panel_nozzle_dia->SetSizer(nozzle_dia_sizer); @@ -2653,77 +2758,84 @@ Sidebar::Sidebar(Plater *parent) p->panel_printer_bed->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->panel_printer_bed->SetBorderColor(panel_color.bd_normal); p->panel_printer_bed->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); - p->panel_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { + p->panel_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { on_leave_image_printer_bed(evt); p->combo_printer_bed->wxEvtHandler::ProcessEvent(evt); }); - //ScalableButton *wiki_bed = new ScalableButton(p->panel_printer_bed, wxID_ANY, "help"); - //wiki_bed->Bind(wxEVT_BUTTON, [](wxCommandEvent) { - // wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/x1/manual/compatibility-and-parameter-settings-of-filaments"); - //}); + // ScalableButton *wiki_bed = new ScalableButton(p->panel_printer_bed, wxID_ANY, "help"); + // wiki_bed->Bind(wxEVT_BUTTON, [](wxCommandEvent) { + // wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/x1/manual/compatibility-and-parameter-settings-of-filaments"); + // }); ScalableBitmap bitmap_bed(p->panel_printer_bed, "printer_placeholder", PRINTER_THUMBNAIL_SIZE.GetHeight()); p->image_printer_bed = new wxStaticBitmap(p->panel_printer_bed, wxID_ANY, bitmap_bed.bmp(), wxDefaultPosition, wxDefaultSize, 0); - p->image_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { + p->image_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { on_leave_image_printer_bed(evt); p->combo_printer_bed->wxEvtHandler::ProcessEvent(evt); }); - p->combo_printer_bed = new ComboBox(p->panel_printer_bed, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + p->combo_printer_bed = new ComboBox(p->panel_printer_bed, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, + wxCB_READONLY); p->combo_printer_bed->SetBorderWidth(0); p->combo_printer_bed->GetDropDown().SetUseContentWidth(true); - p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow - p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow + p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow reset_bed_type_combox_choices(true); - p->combo_printer_bed->Bind(wxEVT_COMBOBOX, [this](auto &e) { - auto image_path = get_cur_select_bed_image(); + p->combo_printer_bed->Bind(wxEVT_COMBOBOX, [this](auto& e) { + auto image_path = get_cur_select_bed_image(); p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); e.Skip(); }); // ORCA paint whole combobox on focus - auto bed_focus_bg = [this, panel_color](bool focused){ + auto bed_focus_bg = [this, panel_color](bool focused) { auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); p->panel_printer_bed->SetBackgroundColor(bg_color); p->panel_printer_bed->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); p->image_printer_bed->SetBackgroundColour(bg_color); p->combo_printer_bed->SetBackgroundColour(bg_color); // paints margins instead combo background }; - p->combo_printer_bed->Bind(wxEVT_SET_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(true ); e.Skip();}); - p->combo_printer_bed->Bind(wxEVT_KILL_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(false); e.Skip();}); + p->combo_printer_bed->Bind(wxEVT_SET_FOCUS, [this, bed_focus_bg](auto& e) { + bed_focus_bg(true); + e.Skip(); + }); + p->combo_printer_bed->Bind(wxEVT_KILL_FOCUS, [this, bed_focus_bg](auto& e) { + bed_focus_bg(false); + e.Skip(); + }); // highlight border on hover auto printer_bed_hovered = std::make_shared>(); - for (wxWindow *w : std::initializer_list{p->panel_printer_bed, p->image_printer_bed, p->combo_printer_bed}) { - w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent &e) { + for (wxWindow* w : std::initializer_list{p->panel_printer_bed, p->image_printer_bed, p->combo_printer_bed}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent& e) { printer_bed_hovered->insert(w); - if(!p->combo_printer_bed->HasFocus()) + if (!p->combo_printer_bed->HasFocus()) p->panel_printer_bed->SetBorderColor(panel_color.bd_hover); - if(w == p->image_printer_bed && !p->combo_printer_bed->is_drop_down()) // dont trigger while combo open + if (w == p->image_printer_bed && !p->combo_printer_bed->is_drop_down()) // dont trigger while combo open on_enter_image_printer_bed(e); e.Skip(); }); - w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent &e) { + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent& e) { printer_bed_hovered->erase(w); if (printer_bed_hovered->empty() && !p->combo_printer_bed->HasFocus()) p->panel_printer_bed->SetBorderColor(panel_color.bd_normal); - if(w == p->image_printer_bed) + if (w == p->image_printer_bed) on_leave_image_printer_bed(e); e.Skip(); }); } - wxBoxSizer *bed_type_sizer = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* bed_type_sizer = new wxBoxSizer(wxHORIZONTAL); bed_type_sizer->Add(p->combo_printer_bed, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(2)); bed_type_sizer->Add(p->image_printer_bed, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(2)); p->panel_printer_bed->SetSizer(bed_type_sizer); - AppConfig *app_config = wxGetApp().app_config; + AppConfig* app_config = wxGetApp().app_config; std::string str_bed_type = app_config->get("curr_bed_type"); - int bed_type_value = atoi(str_bed_type.c_str()); + int bed_type_value = atoi(str_bed_type.c_str()); // hotfix: btDefault is added as the first one in BedType, and app_config should not be btDefault if (bed_type_value == 0) { app_config->set("curr_bed_type", "1"); @@ -2740,7 +2852,7 @@ Sidebar::Sidebar(Plater *parent) if (item.first == str_bed_type) bed_type = (BedType)item.second; }*/ - BedType bed_type = (BedType)bed_type_value; + BedType bed_type = (BedType) bed_type_value; project_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); /* ORCA THIS PART MOVED TO TITLEBAR @@ -2770,13 +2882,10 @@ Sidebar::Sidebar(Plater *parent) }); p->btn_sync_printer = btn_sync; */ - p->timer_sync_printer->Bind(wxEVT_TIMER, [this] (wxTimerEvent & e) { - p->flush_printer_sync(); - }); - + p->timer_sync_printer->Bind(wxEVT_TIMER, [this](wxTimerEvent& e) { p->flush_printer_sync(); }); - p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); - p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); + p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); + p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); p->single_extruder = new ExtruderGroup(p->m_panel_printer_content, -1, _L("Nozzle")); // manuallySetNozzleCount refreshes the badge and the plater itself; it is a no-op unless the // printer has a multi-nozzle extruder (and the edit button is only enabled then, see @@ -2786,24 +2895,24 @@ Sidebar::Sidebar(Plater *parent) p->single_extruder->SetEditEnabled(false); // Orca: keep the floating switcher icon aligned with the left extruder's AMS row when the // extruder card is resized (the overlay is absolutely positioned, not managed by a sizer). - p->left_extruder->Bind(wxEVT_SIZE, [this](wxSizeEvent &evt) { + p->left_extruder->Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) { if (p->extruder_separator_icon && p->extruder_separator_icon->IsShown()) { wxPoint left_box_pos = p->left_extruder->GetPosition(); wxPoint ams_local_pos = p->left_extruder->hsizer_ams->GetPosition(); - wxSize left_size = p->left_extruder->sizer->GetSize(); - wxSize ams_size = p->left_extruder->hsizer_ams->GetSize(); - wxSize icon_size = p->extruder_separator_icon->GetSize(); - int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); - int center_x = left_size.GetWidth() + FromDIP(6) - icon_size.GetWidth() / 2; - int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2 - icon_size.GetHeight() / 2; + wxSize left_size = p->left_extruder->sizer->GetSize(); + wxSize ams_size = p->left_extruder->hsizer_ams->GetSize(); + wxSize icon_size = p->extruder_separator_icon->GetSize(); + int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); + int center_x = left_size.GetWidth() + FromDIP(6) - icon_size.GetWidth() / 2; + int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2 - icon_size.GetHeight() / 2; p->extruder_separator_icon->SetPosition(wxPoint(center_x, center_y)); if (p->m_panel_printer_content) p->m_panel_printer_content->Refresh(); } evt.Skip(); }); - auto switch_diameter = [this](wxCommandEvent & evt) { - auto extruder = dynamic_cast(dynamic_cast(evt.GetEventObject())->GetParent()); + auto switch_diameter = [this](wxCommandEvent& evt) { + auto extruder = dynamic_cast(dynamic_cast(evt.GetEventObject())->GetParent()); p->is_switching_diameter = true; p->switch_diameter(extruder == p->single_extruder); p->is_switching_diameter = false; @@ -2820,252 +2929,394 @@ Sidebar::Sidebar(Plater *parent) } { + // Orca: Sidebar - Filament titlebar UI + // add filament title + p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_filament_title->SetBackgroundColor(title_bg); + p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { + if (!p || !p->m_filament_area_wrapper || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || + !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn) + return; + // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament + // button also block fold/unfold feature when user clicks to spacing between icons + int exclude_pt = p->m_bpButton_set_filament->GetPosition().x; // maximum fixed item + if (p->m_purge_mode_btn->IsShown()) + exclude_pt = p->m_purge_mode_btn->GetPosition().x; + else if (p->m_flushing_volume_btn->IsShown()) + exclude_pt = p->m_flushing_volume_btn->GetPosition().x; + else if (p->m_bpButton_add_filament->IsShown()) + exclude_pt = p->m_bpButton_add_filament->GetPosition().x - FromDIP(30); // reserve spacing for delete button + else if (ams_btn->IsShown()) + exclude_pt = ams_btn->GetPosition().x; + if (e.GetPosition().x > exclude_pt) + return; + bool isShown = p->m_filament_area_wrapper->IsShown(); + p->m_filament_area_wrapper->Show(!isShown); + p->m_panel_filament_separator->Show(isShown); + m_scrolled_sizer->Layout(); - // Orca: Sidebar - Filament titlebar UI - // add filament title - p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_filament_title->SetBackgroundColor(title_bg); - p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); - p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { - if (!p || !p->m_panel_filament_content || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn) - return; - // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button - // also block fold/unfold feature when user clicks to spacing between icons - int exclude_pt = p->m_bpButton_set_filament->GetPosition().x; // maximum fixed item - if (p->m_purge_mode_btn->IsShown()) exclude_pt = p->m_purge_mode_btn->GetPosition().x; - else if (p->m_flushing_volume_btn->IsShown()) exclude_pt = p->m_flushing_volume_btn->GetPosition().x; - else if (p->m_bpButton_add_filament->IsShown()) exclude_pt = p->m_bpButton_add_filament->GetPosition().x - FromDIP(30); // reserve spacing for delete button - else if (ams_btn->IsShown()) exclude_pt = ams_btn->GetPosition().x; - if (e.GetPosition().x > exclude_pt) - return; - bool isShown = p->m_panel_filament_content->IsShown(); - p->m_panel_filament_content->Show(!isShown); - p->m_panel_filament_separator->Show(isShown); - m_scrolled_sizer->Layout(); - - CallAfter([this]{update_filaments_counter(true);}); // call after all UI processing done - }); - - wxBoxSizer* bSizer39; - bSizer39 = new wxBoxSizer( wxHORIZONTAL ); - p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); - p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Project Filaments"), LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - bSizer39->Add(p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); - bSizer39->SetMinSize(-1, FromDIP(30)); - - p->m_staticText_filament_count = new Label(p->m_panel_filament_title, "(0)", LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add(p->m_staticText_filament_count, 0, wxALIGN_CENTER ); - bSizer39->Add(FromDIP(10), 0, 0, 0, 0); - - p->m_panel_filament_title->SetSizer( bSizer39 ); - p->m_panel_filament_title->Layout(); - scrolled_sizer->Add(p->m_panel_filament_title, 0, wxEXPAND | wxALL, 0); - // ORCA add bottom border for seperation wile sections folded - p->m_panel_filament_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string - p->m_panel_filament_separator->SetBackgroundColour("#FFFFFF"); - scrolled_sizer->Add(p->m_panel_filament_separator, 0, wxEXPAND); - - bSizer39->AddStretchSpacer(1); - - p->m_purge_mode_btn = new Button(p->m_panel_filament_title, _L("Purge mode")); - p->m_purge_mode_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - - p->m_purge_mode_btn->Bind(wxEVT_BUTTON, [](wxCommandEvent &e) { - auto &preset_bundle = *wxGetApp().preset_bundle; - auto support_fast_purge_opt = preset_bundle.printers.get_edited_preset().config.option("support_fast_purge_mode"); - bool support_fast_purge = support_fast_purge_opt ? support_fast_purge_opt->value : false; - auto dlg_type = support_fast_purge ? PurgeModeDialogType::FastMode : PurgeModeDialogType::MultiNozzle; - PurgeModeDialog dlg(static_cast(wxGetApp().mainframe), dlg_type); - if (dlg.ShowModal() == wxID_OK) { - preset_bundle.project_config.set_key_value("prime_volume_mode", new ConfigOptionEnum(dlg.get_selected_mode())); - wxGetApp().plater()->update(); - } - }); - - bSizer39->Add(p->m_purge_mode_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - bSizer39->Hide(p->m_purge_mode_btn); // hidden on launch; shown only for printers that support purge mode selection - - // BBS - // add wiping dialog - //wiping_dialog_button->SetFont(wxGetApp().normal_font()); - p->m_flushing_volume_btn = new Button(p->m_panel_filament_title, _L("Flushing volumes")); - p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_flushing_volume_btn->SetId(wxID_RESET); - auto has_modify = is_flush_config_modified(); - set_flushing_volume_warning(has_modify); - - p->m_flushing_volume_btn->Bind(wxEVT_BUTTON, ([parent, this](wxCommandEvent &e) { - open_flushing_dialog(parent, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, parent)); - p->plater->get_view3D_canvas3D()->reload_scene(true); - p->plater->update(); - })); - - bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - bSizer39->Hide(p->m_flushing_volume_btn); // ORCA Ensure button is hidden on launch while 1 filament exist - - ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); - add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ - add_filament(); - update_filaments_counter(); - }); - p->m_bpButton_add_filament = add_btn; - - // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - - ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); - del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { - delete_filament(); - update_filaments_counter(); - }); - p->m_bpButton_del_filament = del_btn; - - bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); - bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - - bSizer39->Hide(p->m_bpButton_del_filament); // ORCA Ensure button is hidden on launch while 1 filament exist - - ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, - wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 - ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); - ams_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { - sync_ams_list(); - }); - - ams_btn->Bind(wxEVT_UPDATE_UI, &Sidebar::update_sync_ams_btn_enable, this); - p->m_bpButton_ams_filament = ams_btn; - - bSizer39->Add(ams_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); - //bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); - - ScalableButton* set_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "settings"); - set_btn->SetToolTip(_L("Set filaments to use")); - set_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { - p->editing_filament = -1; - // wxGetApp().params_dialog()->Popup(); - // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); - wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); + CallAfter([this] { update_filaments_counter(true); }); // call after all UI processing done }); - p->m_bpButton_set_filament = set_btn; - bSizer39->Add(set_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); - bSizer39->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + wxBoxSizer* bSizer39; + bSizer39 = new wxBoxSizer(wxHORIZONTAL); + p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Project Filaments"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + bSizer39->Add(p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); + bSizer39->SetMinSize(-1, FromDIP(30)); - // add filament content - p->m_panel_filament_content = new wxScrolledWindow( p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); - p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); - p->m_panel_filament_content->SetScrollRate(0, 5); - //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); - p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); + p->m_staticText_filament_count = new Label(p->m_panel_filament_title, "(0)", LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_staticText_filament_count, 0, wxALIGN_CENTER); + bSizer39->Add(FromDIP(10), 0, 0, 0, 0); - //wxBoxSizer* bSizer_filament_content; - //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); + p->m_panel_filament_title->SetSizer(bSizer39); + p->m_panel_filament_title->Layout(); + scrolled_sizer->Add(p->m_panel_filament_title, 0, wxEXPAND | wxALL, 0); + // ORCA add bottom border for seperation wile sections folded + p->m_panel_filament_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, + wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string + p->m_panel_filament_separator->SetBackgroundColour("#FFFFFF"); + scrolled_sizer->Add(p->m_panel_filament_separator, 0, wxEXPAND); - // Orca: Sidebar - Filament content UI: setup filament selection combos panel layout - // Creates a two-column grid layout for filament selection dropdowns within the scrollable panel - p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); - p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + bSizer39->AddStretchSpacer(1); - p->combos_filament.push_back(nullptr); + p->m_purge_mode_btn = new Button(p->m_panel_filament_title, _L("Purge mode")); + p->m_purge_mode_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - /* first filament item */ - init_filament_combo(&p->combos_filament[0], 0); + p->m_purge_mode_btn->Bind(wxEVT_BUTTON, [](wxCommandEvent& e) { + auto& preset_bundle = *wxGetApp().preset_bundle; + auto support_fast_purge_opt = preset_bundle.printers.get_edited_preset().config.option( + "support_fast_purge_mode"); + bool support_fast_purge = support_fast_purge_opt ? support_fast_purge_opt->value : false; + auto dlg_type = support_fast_purge ? PurgeModeDialogType::FastMode : PurgeModeDialogType::MultiNozzle; + PurgeModeDialog dlg(static_cast(wxGetApp().mainframe), dlg_type); + if (dlg.ShowModal() == wxID_OK) { + preset_bundle.project_config.set_key_value("prime_volume_mode", + new ConfigOptionEnum(dlg.get_selected_mode())); + wxGetApp().plater()->update(); + } + }); - //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); - wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); - sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); - p->m_panel_filament_content->SetSizer(sizer_filaments2); - p->m_panel_filament_content->Layout(); - - update_filaments_area_height(); // ORCA + bSizer39->Add(p->m_purge_mode_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + bSizer39->Hide(p->m_purge_mode_btn); // hidden on launch; shown only for printers that support purge mode selection - scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament + // BBS + // add wiping dialog + // wiping_dialog_button->SetFont(wxGetApp().normal_font()); + p->m_flushing_volume_btn = new Button(p->m_panel_filament_title, _L("Flushing volumes")); + p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_flushing_volume_btn->SetId(wxID_RESET); + auto has_modify = is_flush_config_modified(); + set_flushing_volume_warning(has_modify); + + p->m_flushing_volume_btn->Bind(wxEVT_BUTTON, ([parent, this](wxCommandEvent& e) { + open_flushing_dialog(parent, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, parent)); + p->plater->get_view3D_canvas3D()->reload_scene(true); + p->plater->update(); + })); + + bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + bSizer39->Hide(p->m_flushing_volume_btn); // ORCA Ensure button is hidden on launch while 1 filament exist + + ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e) { + add_filament(); + update_filaments_counter(); + }); + p->m_bpButton_add_filament = add_btn; + + // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e) { + delete_filament(); + update_filaments_counter(); + }); + p->m_bpButton_del_filament = del_btn; + + bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, + FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position + // change when remove icon automatically hidden + + bSizer39->Hide(p->m_bpButton_del_filament); // ORCA Ensure button is hidden on launch while 1 filament exist + + ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 + ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); + ams_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e) { sync_ams_list(); }); + + ams_btn->Bind(wxEVT_UPDATE_UI, &Sidebar::update_sync_ams_btn_enable, this); + p->m_bpButton_ams_filament = ams_btn; + + bSizer39->Add(ams_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); + // bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); + + ScalableButton* set_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "settings"); + set_btn->SetToolTip(_L("Set filaments to use")); + set_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + p->editing_filament = -1; + // wxGetApp().params_dialog()->Popup(); + // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); + }); + p->m_bpButton_set_filament = set_btn; + + bSizer39->Add(set_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); + bSizer39->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + + // ---- Wrapper panel for collapse/expand of all filament content ---- + p->m_filament_area_wrapper = new wxPanel(p->scrolled, wxID_ANY); + p->m_filament_area_wrapper->SetBackgroundColour(*wxWHITE); + auto* wrapper_sizer = new wxBoxSizer(wxVERTICAL); + + // add filament content + p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxTAB_TRAVERSAL); + p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); + p->m_panel_filament_content->SetScrollRate(0, 5); + // p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); + p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); + + // wxBoxSizer* bSizer_filament_content; + // bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); + + // Orca: Sidebar - Filament content UI: setup filament selection combos panel layout + // Creates a two-column grid layout for filament selection dropdowns within the scrollable panel + p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); + p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + + p->combos_filament.push_back(nullptr); + + /* first filament item */ + init_filament_combo(&p->combos_filament[0], 0); + + // bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); + wxSizer* sizer_filaments2 = new wxBoxSizer(wxVERTICAL); + sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); + p->m_panel_filament_content->SetSizer(sizer_filaments2); + p->m_panel_filament_content->Layout(); + + update_filaments_area_height(); // ORCA + + wrapper_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND); + + // ---- Mixed-color filament section ---- + // A mixed filament is a virtual slot realized from 2-3 physical filaments at slicing time. + // Everything here stays hidden until at least two physical filaments exist, so a single + // filament setup looks exactly as before. + { + // 1) "+ Add Mixed Filament" button, shown only while no mixed filament exists yet. + p->m_btn_add_mixed_filament = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_btn_add_mixed_filament->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); + p->m_btn_add_mixed_filament->SetMinSize(wxSize(-1, FromDIP(23))); + { + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* icon_add = new ScalableButton(p->m_btn_add_mixed_filament, wxID_ANY, "add_filament", wxEmptyString, wxDefaultSize, + wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); + auto* add_label = new wxStaticText(p->m_btn_add_mixed_filament, wxID_ANY, _L("Add Mixed Filament"), wxDefaultPosition, + wxDefaultSize, 0); + add_label->SetFont(::Label::Body_13); + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(icon_add, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + btn_sizer->Add(add_label, 0, wxALIGN_CENTER_VERTICAL); + btn_sizer->AddStretchSpacer(); + p->m_btn_add_mixed_filament->SetSizer(btn_sizer); + p->m_btn_add_mixed_filament->SetCursor(wxCursor(wxCURSOR_HAND)); + // Whole panel is the hit target, so forward clicks from the children too. + auto on_click = [this](wxMouseEvent&) { add_mixed_filament(); }; + p->m_btn_add_mixed_filament->Bind(wxEVT_LEFT_UP, on_click); + add_label->Bind(wxEVT_LEFT_UP, on_click); + icon_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); + } + wrapper_sizer->Add(p->m_btn_add_mixed_filament, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, FromDIP(8)); + + // 2) Title row with add / remove buttons, shown once a mixed filament exists. + p->m_panel_mixed_title = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_panel_mixed_title->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); + p->m_text_mixed_title = new wxStaticText(p->m_panel_mixed_title, wxID_ANY, _L("Mixed Filament")); + p->m_text_mixed_title->SetFont(::Label::Head_14); + title_sizer->Add(p->m_text_mixed_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + title_sizer->AddStretchSpacer(); + + p->m_btn_mixed_del = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_filament"); + p->m_btn_mixed_del->SetToolTip(_L("Remove last mixed filament")); + p->m_btn_mixed_del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + auto* plater_ptr = dynamic_cast(GetParent()); + if (!plater_ptr) + return; + auto mixed_indices = plater_ptr->mixed_filament_config_indices(); + if (!mixed_indices.empty()) + delete_mixed_filament_at(mixed_indices.size() - 1); + }); + title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + + p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament"); + p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament")); + p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); + title_sizer->Add(p->m_btn_mixed_add, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + title_sizer->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + + p->m_panel_mixed_title->SetSizer(title_sizer); + } + wrapper_sizer->Add(p->m_panel_mixed_title, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); + + // 3) Mixed filament rows, in their own scroll area so a long mixed list does not + // push the physical filament list off screen. + p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxTAB_TRAVERSAL); + p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); + p->m_mixed_scroll_area->SetScrollRate(0, 5); + p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); + p->m_panel_mixed_content = new wxPanel(p->m_mixed_scroll_area, wxID_ANY); + p->m_panel_mixed_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Two columns, same idiom as sizer_filaments. + p->m_sizer_mixed_filaments = new wxBoxSizer(wxHORIZONTAL); + p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + + auto* sizer_mixed2 = new wxBoxSizer(wxVERTICAL); + sizer_mixed2->Add(p->m_sizer_mixed_filaments, 0, wxEXPAND, 0); + p->m_panel_mixed_content->SetSizer(sizer_mixed2); + mix_scroll_sizer->Add(p->m_panel_mixed_content, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + p->m_mixed_scroll_area->SetSizer(mix_scroll_sizer); + } + p->m_mixed_scroll_area->EnableScrolling(false, true); + p->m_mixed_scroll_area->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); + p->m_mixed_scroll_area->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { + int w = p->m_mixed_scroll_area->GetClientSize().GetWidth(); + if (w > 0) + p->m_mixed_scroll_area->SetVirtualSize(w, p->m_mixed_scroll_area->GetVirtualSize().GetHeight()); + e.Skip(); + }); + wrapper_sizer->Add(p->m_mixed_scroll_area, 0, wxEXPAND, 0); + + // 4) Warning bar for mixes whose components were deleted or whose types disagree. + p->m_panel_mixed_warning = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_panel_mixed_warning->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* warn_sizer = new wxBoxSizer(wxHORIZONTAL); + p->m_text_mixed_warning = + new wxStaticText(p->m_panel_mixed_warning, wxID_ANY, + _L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); + p->m_text_mixed_warning->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B"))); + p->m_text_mixed_warning->SetFont(::Label::Body_12); + p->m_text_mixed_warning->Wrap(FromDIP(360)); + warn_sizer->Add(p->m_text_mixed_warning, 1, wxALL, FromDIP(6)); + p->m_panel_mixed_warning->SetSizer(warn_sizer); + } + wrapper_sizer->Add(p->m_panel_mixed_warning, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(8)); + + // Hidden until update_mixed_filament_list() decides otherwise. + p->m_btn_add_mixed_filament->Hide(); + p->m_panel_mixed_title->Hide(); + p->m_mixed_scroll_area->Hide(); + p->m_panel_mixed_content->Hide(); + p->m_panel_mixed_warning->Hide(); + } + // ---- End mixed-color filament section ---- + + p->m_filament_area_wrapper->SetSizer(wrapper_sizer); + p->m_filament_area_wrapper->Layout(); + scrolled_sizer->Add( + p->m_filament_area_wrapper, 0, wxEXPAND | wxTOP | wxBOTTOM, + FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament + // ---- End filament area ---- } { - //add project title - auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; - if (params_panel) { - params_panel->get_top_panel()->Reparent(p->scrolled); - auto spliter_1 = new ::StaticLine(p->scrolled); - spliter_1->SetLineColour("#A6A9AA"); - scrolled_sizer->Add(spliter_1, 0, wxEXPAND); - scrolled_sizer->Add(params_panel->get_top_panel(), 0, wxEXPAND); - auto spliter_2 = new ::StaticLine(p->scrolled); - spliter_2->SetLineColour("#CECECE"); - scrolled_sizer->Add(spliter_2, 0, wxEXPAND); - } - - //add project content - p->sizer_params = new wxBoxSizer(wxVERTICAL); - - // ORCA: Update search box to modern style - p->m_search_bar = new StaticBox(p->scrolled); - p->m_search_bar->SetCornerRadius(0); - p->m_search_bar->SetBorderColor(wxColour("#CECECE")); - - p->m_search_item = new TextInput(p->m_search_bar, wxEmptyString, wxEmptyString, "", wxDefaultPosition, wxDefaultSize, 0 | wxBORDER_NONE); - p->m_search_item->SetIcon(*BitmapCache().load_svg("search", FromDIP(16), FromDIP(16))); // ORCA: Add search icon to search box - - wxTextCtrl* text_ctrl = p->m_search_item->GetTextCtrl(); - text_ctrl->SetHint(_L("Search plate, object and part.")); - text_ctrl->SetForegroundColour(wxColour("#262E30")); - text_ctrl->SetFont(Label::Body_13); - text_ctrl->SetSize(wxSize(-1, FromDIP(16))); // Centers text vertically - - text_ctrl->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) { - if (p->dia->IsShown()) { - e.Skip(); - return; + // add project title + auto params_panel = ((MainFrame*) parent->GetParent())->m_param_panel; + if (params_panel) { + params_panel->get_top_panel()->Reparent(p->scrolled); + auto spliter_1 = new ::StaticLine(p->scrolled); + spliter_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_1, 0, wxEXPAND); + scrolled_sizer->Add(params_panel->get_top_panel(), 0, wxEXPAND); + auto spliter_2 = new ::StaticLine(p->scrolled); + spliter_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_2, 0, wxEXPAND); } - p->m_search_bar->SetBorderColor(wxColour("#009688")); - wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0)); + + // add project content + p->sizer_params = new wxBoxSizer(wxVERTICAL); + + // ORCA: Update search box to modern style + p->m_search_bar = new StaticBox(p->scrolled); + p->m_search_bar->SetCornerRadius(0); + p->m_search_bar->SetBorderColor(wxColour("#CECECE")); + + p->m_search_item = new TextInput(p->m_search_bar, wxEmptyString, wxEmptyString, "", wxDefaultPosition, wxDefaultSize, + 0 | wxBORDER_NONE); + p->m_search_item->SetIcon(*BitmapCache().load_svg("search", FromDIP(16), FromDIP(16))); // ORCA: Add search icon to search box + + wxTextCtrl* text_ctrl = p->m_search_item->GetTextCtrl(); + text_ctrl->SetHint(_L("Search plate, object and part.")); + text_ctrl->SetForegroundColour(wxColour("#262E30")); + text_ctrl->SetFont(Label::Body_13); + text_ctrl->SetSize(wxSize(-1, FromDIP(16))); // Centers text vertically + + text_ctrl->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) { + if (p->dia->IsShown()) { + e.Skip(); + return; + } + p->m_search_bar->SetBorderColor(wxColour("#009688")); + wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0)); #ifndef __WXGTK__ - pos.y += this->p->m_search_bar->GetRect().height; + pos.y += this->p->m_search_bar->GetRect().height; #else this->p->m_search_item->Enable(false); #endif - p->dia->SetPosition(pos); - p->dia->Popup(); - e.Skip(); // required to show caret - }); + p->dia->SetPosition(pos); + p->dia->Popup(); + e.Skip(); // required to show caret + }); - auto search_sizer = new wxBoxSizer(wxHORIZONTAL); - search_sizer->Add(new wxWindow(p->m_search_bar, wxID_ANY, wxDefaultPosition, wxSize(0, 0)), 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(1)); - search_sizer->Add(p->m_search_item, 1, wxEXPAND | wxALL, FromDIP(2)); - p->m_search_bar->SetSizer(search_sizer); - p->m_search_bar->Layout(); - search_sizer->Fit(p->m_search_bar); + auto search_sizer = new wxBoxSizer(wxHORIZONTAL); + search_sizer->Add(new wxWindow(p->m_search_bar, wxID_ANY, wxDefaultPosition, wxSize(0, 0)), 0, wxEXPAND | wxLEFT | wxRIGHT, + FromDIP(1)); + search_sizer->Add(p->m_search_item, 1, wxEXPAND | wxALL, FromDIP(2)); + p->m_search_bar->SetSizer(search_sizer); + p->m_search_bar->Layout(); + search_sizer->Fit(p->m_search_bar); - p->m_object_list = new ObjectList(p->scrolled); - p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) { + p->m_object_list = new ObjectList(p->scrolled); + p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) { #ifdef __WXGTK__ - this->p->m_search_item->Enable(true); + this->p->m_search_item->Enable(true); #endif - this->p->m_search_bar->SetBorderColor(wxColour("#CECECE")); - this->p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when close - }); + this->p->m_search_bar->SetBorderColor(wxColour("#CECECE")); + this->p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when close + }); - p->sizer_params->Add(p->m_search_bar, 0, wxALL | wxEXPAND, 0); - p->sizer_params->Add(p->m_object_list, 1, wxEXPAND | wxTOP, 0); - scrolled_sizer->Add(p->sizer_params, 2, wxEXPAND | wxLEFT, 0); - p->m_object_list->Hide(); - p->m_search_bar->Hide(); - // Frequently Object Settings - p->object_settings = new ObjectSettings(p->scrolled); + p->sizer_params->Add(p->m_search_bar, 0, wxALL | wxEXPAND, 0); + p->sizer_params->Add(p->m_object_list, 1, wxEXPAND | wxTOP, 0); + scrolled_sizer->Add(p->sizer_params, 2, wxEXPAND | wxLEFT, 0); + p->m_object_list->Hide(); + p->m_search_bar->Hide(); + // Frequently Object Settings + p->object_settings = new ObjectSettings(p->scrolled); - p->dia = new Search::SearchObjectDialog(p->m_object_list, p->scrolled->GetParent(), p->m_search_item); + p->dia = new Search::SearchObjectDialog(p->m_object_list, p->scrolled->GetParent(), p->m_search_item); #if !NEW_OBJECT_SETTING - p->object_settings->Hide(); - p->sizer_params->Add(p->object_settings->get_sizer(), 0, wxEXPAND | wxTOP, 5 * em / 10); + p->object_settings->Hide(); + p->sizer_params->Add(p->object_settings->get_sizer(), 0, wxEXPAND | wxTOP, 5 * em / 10); #else - if (params_panel) { - params_panel->Reparent(p->scrolled); - scrolled_sizer->Add(params_panel, 3, wxEXPAND); - } + if (params_panel) { + params_panel->Reparent(p->scrolled); + scrolled_sizer->Add(params_panel, 3, wxEXPAND); + } #endif } @@ -3073,67 +3324,71 @@ Sidebar::Sidebar(Plater *parent) p->object_layers->Hide(); p->sizer_params->Add(p->object_layers->get_sizer(), 0, wxEXPAND | wxTOP, 0); - auto *sizer = new wxBoxSizer(wxVERTICAL); + auto* sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(p->scrolled, 1, wxEXPAND); SetSizer(sizer); } Sidebar::~Sidebar() {} -void Sidebar::on_enter_image_printer_bed(wxMouseEvent &evt) { - //p->image_printer_bed->Bind(wxEVT_LEAVE_WINDOW, &Sidebar::on_leave_image_printer_bed, this); - auto pos = p->panel_printer_bed->GetScreenPosition(); - auto rect = p->panel_printer_bed->GetRect(); - wxPoint temp_pos(pos.x + rect.GetWidth() + FromDIP(3), pos.y); +void Sidebar::on_enter_image_printer_bed(wxMouseEvent& evt) +{ + // p->image_printer_bed->Bind(wxEVT_LEAVE_WINDOW, &Sidebar::on_leave_image_printer_bed, this); + auto pos = p->panel_printer_bed->GetScreenPosition(); + auto rect = p->panel_printer_bed->GetRect(); + wxPoint temp_pos(pos.x + rect.GetWidth() + FromDIP(3), pos.y); if (p->big_bed_image_popup == nullptr) p->big_bed_image_popup = new ImageDPIFrame(); - auto image_path = get_cur_select_bed_image(); - p->big_bed_image_popup->set_bitmap(create_scaled_bitmap("big_" + image_path, p->big_bed_image_popup, p->big_bed_image_popup->get_image_px())); + auto image_path = get_cur_select_bed_image(); + p->big_bed_image_popup->set_bitmap( + create_scaled_bitmap("big_" + image_path, p->big_bed_image_popup, p->big_bed_image_popup->get_image_px())); p->big_bed_image_popup->set_title(p->combo_printer_bed->GetString(p->combo_printer_bed->GetSelection())); p->big_bed_image_popup->SetCanFocus(false); p->big_bed_image_popup->SetPosition(temp_pos); p->big_bed_image_popup->on_show(); } -void Sidebar::on_leave_image_printer_bed(wxMouseEvent &evt) { - //auto pos_x = evt.GetX(); - //auto pos_y = evt.GetY(); - //auto rect = p->image_printer_bed->GetRect(); - //if ((pos_x <= 0 || pos_y <= 0 || pos_x >= rect.GetWidth()) && p->big_bed_image_popup) { +void Sidebar::on_leave_image_printer_bed(wxMouseEvent& evt) +{ + // auto pos_x = evt.GetX(); + // auto pos_y = evt.GetY(); + // auto rect = p->image_printer_bed->GetRect(); + // if ((pos_x <= 0 || pos_y <= 0 || pos_x >= rect.GetWidth()) && p->big_bed_image_popup) { if (p->big_bed_image_popup) { bool was_visible = p->big_bed_image_popup->IsShown(); p->big_bed_image_popup->on_hide(); - if(!p->combo_printer_bed->is_drop_down() && was_visible) - p->combo_printer_bed->SetFocus(); // set focus back to bed type combo. this prevents weird look if focus on other item + if (!p->combo_printer_bed->is_drop_down() && was_visible) + p->combo_printer_bed->SetFocus(); // set focus back to bed type combo. this prevents weird look if focus on other item } } -void Sidebar::on_change_color_mode(bool is_dark) { - const ModelObjectPtrs &mos = wxGetApp().model().objects; +void Sidebar::on_change_color_mode(bool is_dark) +{ + const ModelObjectPtrs& mos = wxGetApp().model().objects; for (int i = 0; i < mos.size(); i++) { - wxGetApp().obj_list()->update_info_items(i,nullptr,false,true); + wxGetApp().obj_list()->update_info_items(i, nullptr, false, true); } - } void Sidebar::create_printer_preset() { CreatePrinterPresetDialog dlg(wxGetApp().mainframe); - int res = dlg.ShowModal(); + int res = dlg.ShowModal(); if (wxID_OK == res) { wxGetApp().load_current_presets(); wxGetApp().mainframe->update_side_preset_ui(); update_ui_from_settings(); update_all_preset_comboboxes(); CreatePresetSuccessfulDialog success_dlg(wxGetApp().mainframe, SuccessType::PRINTER); - int res = success_dlg.ShowModal(); + int res = success_dlg.ShowModal(); if (res == wxID_OK) { p->editing_filament = -1; - if (p->combo_printer->switch_to_tab()) p->editing_filament = 0; + if (p->combo_printer->switch_to_tab()) + p->editing_filament = 0; } } } -void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) +void Sidebar::init_filament_combo(PlaterPresetComboBox** combo, const int filament_idx) { *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); (*combo)->set_filament_idx(filament_idx); @@ -3147,8 +3402,10 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame combo_and_btn_sizer->AddSpacer(FromDIP((filament_idx % 2) == 0 ? 12 : 3)); // Content Margin (*combo)->clr_picker->SetLabel(wxString::Format("%d", filament_idx + 1)); - combo_and_btn_sizer->Add((*combo)->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) - combo_and_btn_sizer->Add(*combo, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, 30 * wxGetApp().em_unit() / 10}); // ORCA ensure height matches with PlaterPresetComboBox + combo_and_btn_sizer->Add((*combo)->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, + FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) + combo_and_btn_sizer->Add(*combo, 1, wxALL | wxEXPAND, FromDIP(2)) + ->SetMinSize({-1, 30 * wxGetApp().em_unit() / 10}); // ORCA ensure height matches with PlaterPresetComboBox /* BBS hide del_btn ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "delete_filament"); @@ -3171,16 +3428,15 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame edit_btn->Bind(wxEVT_BUTTON, [this, edit_btn, combobox, filament_idx](wxCommandEvent) { bool single_or_bbl = should_show_SEMM_buttons(); bool is_multi_material = p->combos_filament.size() > 1; - if(single_or_bbl && is_multi_material) { - // MULTI MATERIAL Show menu + if (single_or_bbl && is_multi_material) { + // MULTI MATERIAL Show menu auto menu = p->plater->filament_action_menu(filament_idx); - wxPoint pt { 0, edit_btn->GetSize().GetHeight() + FromDIP(2) }; - pt = edit_btn->ClientToScreen(pt); - pt = wxGetApp().mainframe->ScreenToClient(pt); + wxPoint pt{0, edit_btn->GetSize().GetHeight() + FromDIP(2)}; + pt = edit_btn->ClientToScreen(pt); + pt = wxGetApp().mainframe->ScreenToClient(pt); p->m_menu_filament_id = filament_idx; p->plater->PopupMenu(menu, (int) pt.x, pt.y); - } - else { + } else { // SINGLE MATERIAL / MULTI EXTRUDER / TOOLCHANGER / IDEX Opens Dialog directly p->editing_filament = filament_idx; combobox->switch_to_tab(); @@ -3188,14 +3444,16 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filame }); combobox->edit_btn = edit_btn; - combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) + combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, + FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); // BBS: filament double columns - auto side = filament_idx % 2; - auto /***/sizer_filaments = this->p->sizer_filaments->GetItem(side)->GetSizer(); - if (side == 1 && filament_idx > 1) sizer_filaments->Remove(filament_idx / 2); + auto side = filament_idx % 2; + auto /***/ sizer_filaments = this->p->sizer_filaments->GetItem(side)->GetSizer(); + if (side == 1 && filament_idx > 1) + sizer_filaments->Remove(filament_idx / 2); sizer_filaments->Add(combo_and_btn_sizer, 1, wxEXPAND); if (side == 0 && filament_idx > 0) { sizer_filaments = this->p->sizer_filaments->GetItem(1)->GetSizer(); @@ -3208,14 +3466,14 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count) if (current_extruder_count >= p->combos_filament.size()) return; while (p->combos_filament.size() > current_extruder_count) { - const int last = p->combos_filament.size() - 1; + const int last = p->combos_filament.size() - 1; auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); sizer_filaments->Remove(last / 2); (*p->combos_filament[last]).Destroy(); p->combos_filament.pop_back(); } // BBS: filament double columns - auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t)0)->GetSizer(); + auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); if (current_extruder_count < 2) { sizer_filaments1->Clear(); @@ -3231,27 +3489,27 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count) void Sidebar::update_all_preset_comboboxes() { - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); - auto p_mainframe = wxGetApp().mainframe; - auto cfg = preset_bundle.printers.get_edited_preset().config; - const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + auto p_mainframe = wxGetApp().mainframe; + auto cfg = preset_bundle.printers.get_edited_preset().config; + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || use_printer_agents; if (preset_bundle.use_bbl_network()) { - //only show connection button for not-BBL printer - //p->btn_connect_printer->Hide(); + // only show connection button for not-BBL printer + // p->btn_connect_printer->Hide(); p->m_printer_connect->Hide(); - //only show sync-ams button for BBL printer + // only show sync-ams button for BBL printer p->m_bpButton_ams_filament->Show(); - //update print button default value for bbl or third-party printer + // update print button default value for bbl or third-party printer p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); } else { - //p->btn_connect_printer->Show(); - // ORCA: hide the physical-printer connection button when printer agents are enabled + // p->btn_connect_printer->Show(); + // ORCA: hide the physical-printer connection button when printer agents are enabled p->m_printer_connect->Show(!use_printer_agents); // ORCA: show/hide sync-ams button based on filament sync mode @@ -3264,19 +3522,19 @@ void Sidebar::update_all_preset_comboboxes() // Orca: with "Support 3MF as gcode" (use_3mf) the local export is a .gcode.3mf bundle, so when no // printer host/IP is configured the default action is "Export plate sliced file" (mirrors the // print dropdown) instead of "Export G-code file". - auto print_btn_type = cfg.opt_bool("use_3mf") ? MainFrame::PrintSelectType::eExportSlicedFile - : MainFrame::PrintSelectType::eExportGcode; - wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); + auto print_btn_type = cfg.opt_bool("use_3mf") ? MainFrame::PrintSelectType::eExportSlicedFile : + MainFrame::PrintSelectType::eExportGcode; + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; - if(url.empty()) + if (url.empty()) url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir())); else { const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); - print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) - ? MainFrame::PrintSelectType::ePrintPlate - : MainFrame::PrintSelectType::eSendGcode; + print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) ? + MainFrame::PrintSelectType::ePrintPlate : + MainFrame::PrintSelectType::eSendGcode; } if (use_printer_agents) @@ -3284,29 +3542,27 @@ void Sidebar::update_all_preset_comboboxes() else if (!use_native_device_tab) p_mainframe->load_printer_url(url, apikey); - p_mainframe->set_print_button_to_default(print_btn_type); - } if (cfg.opt_bool("pellet_modded_printer")) { - p->m_staticText_filament_settings->SetLabel(_L("Pellets")); + p->m_staticText_filament_settings->SetLabel(_L("Pellets")); p->m_filament_icon->SetBitmap_("pellets"); } else { - p->m_staticText_filament_settings->SetLabel(_L("Filament")); + p->m_staticText_filament_settings->SetLabel(_L("Filament")); p->m_filament_icon->SetBitmap_("filament"); } show_SEMM_buttons(); - //p->m_staticText_filament_settings->Update(); + // p->m_staticText_filament_settings->Update(); if (is_bbl_vendor || cfg.opt_bool("support_multi_bed_types")) { p->combo_printer_bed->Enable(); // Orca: don't update bed type if loading project if (!p->plater->is_loading_project()) { bool has_changed = reset_bed_type_combox_choices(); - bool flag = m_begin_sync_printer_status && !has_changed; + bool flag = m_begin_sync_printer_status && !has_changed; if (!(flag)) { auto str_bed_type = wxGetApp().app_config->get_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), "curr_bed_type"); @@ -3316,7 +3572,7 @@ void Sidebar::update_all_preset_comboboxes() bed_type_value = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); } - set_bed_type_accord_combox((BedType) bed_type_value); + set_bed_type_accord_combox((BedType) bed_type_value); } else { BedType bed_type = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); set_bed_type_accord_combox(bed_type); @@ -3336,10 +3592,10 @@ void Sidebar::update_all_preset_comboboxes() p->panel_printer_bed->Show(is_bbl_vendor || cfg.opt_bool("support_multi_bed_types")); // Update the print choosers to only contain the compatible presets, update the dirty flags. - //BBS + // BBS // Update the printer choosers, update the dirty flags. - //p->combo_printer->update(); + // p->combo_printer->update(); // Update the filament choosers to only contain the compatible presets, update the color preview, // update the dirty flags. if (print_tech == ptFFF) { @@ -3358,13 +3614,12 @@ void Sidebar::update_all_preset_comboboxes() void Sidebar::update_presets(Preset::Type preset_type) { - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%")%preset_type; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%") % preset_type; switch (preset_type) { - case Preset::TYPE_FILAMENT: - { + case Preset::TYPE_FILAMENT: { // BBS #if 0 const size_t extruder_cnt = print_tech != ptFFF ? 1 : @@ -3373,16 +3628,16 @@ void Sidebar::update_presets(Preset::Type preset_type) #else const size_t filament_cnt = p->combos_filament.size(); #endif - const std::string &name = preset_bundle.filaments.get_selected_preset_name(); + const std::string& name = preset_bundle.filaments.get_selected_preset_name(); if (p->editing_filament >= 0) { preset_bundle.set_filament_preset(p->editing_filament, name); } else if (filament_cnt == 1) { // Single filament printer, synchronize the filament presets. - Preset *preset = preset_bundle.filaments.find_preset(name, false); + Preset* preset = preset_bundle.filaments.find_preset(name, false); if (preset) { - if (preset->is_compatible) preset_bundle.set_filament_preset(0, name); + if (preset->is_compatible) + preset_bundle.set_filament_preset(0, name); } - } for (size_t i = 0; i < filament_cnt; i++) @@ -3393,25 +3648,22 @@ void Sidebar::update_presets(Preset::Type preset_type) } case Preset::TYPE_PRINT: - //wxGetApp().mainframe->m_param_panel; - //p->combo_print->update(); + // wxGetApp().mainframe->m_param_panel; + // p->combo_print->update(); { - Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); - if (print_tab) { - print_tab->get_combo_box()->update(); + Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); + if (print_tab) { + print_tab->get_combo_box()->update(); + } + break; } - break; - } - case Preset::TYPE_SLA_PRINT: - ;// p->combo_sla_print->update(); + case Preset::TYPE_SLA_PRINT:; // p->combo_sla_print->update(); break; - case Preset::TYPE_SLA_MATERIAL: - ;// p->combo_sla_material->update(); + case Preset::TYPE_SLA_MATERIAL:; // p->combo_sla_material->update(); break; - case Preset::TYPE_PRINTER: - { + case Preset::TYPE_PRINTER: { update_all_preset_comboboxes(); p->show_preset_comboboxes(); @@ -3425,17 +3677,17 @@ void Sidebar::update_presets(Preset::Type preset_type) Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); if (auto printer_structure_opt = printer_preset.config.option>("printer_structure")) { - wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3); - } - else + wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = (printer_structure_opt->value == + PrinterStructure::psI3); + } else wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = false; // Update dual extrudes - auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); - auto extruder_variants = printer_preset.config.option("extruder_variant_list"); + auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); + auto extruder_variants = printer_preset.config.option("extruder_variant_list"); std::string printer_model = printer_preset.config.option("printer_model")->value; - bool isBBL = preset_bundle.is_bbl_vendor(); + bool isBBL = preset_bundle.is_bbl_vendor(); bool is_dual_extruder = extruder_variants->size() == 2; // why: agent mode drives the native device tab, so the sidebar lays out like BBL // (no physical-printer connect button). @@ -3446,36 +3698,40 @@ void Sidebar::update_presets(Preset::Type preset_type) // UI left = DEPUTY_EXTRUDER_ID(1), UI right = MAIN_EXTRUDER_ID(0) if (is_dual_extruder) { std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); - auto left_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase); - auto right_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase); + auto left_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, + ToolHeadNameCase::TitleCase); + auto right_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, + ToolHeadNameCase::TitleCase); p->left_extruder->SetTitle(_L(left_title)); p->right_extruder->SetTitle(_L(right_title)); } - auto extruders_def = printer_preset.config.def()->get("extruder_type"); - auto extruders = printer_preset.config.option("extruder_type"); - auto nozzle_volumes_def = wxGetApp().preset_bundle->project_config.def()->get("nozzle_volume_type"); - auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); - auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); - auto diameter = printer_preset.config.opt_string("printer_variant"); + auto extruders_def = printer_preset.config.def()->get("extruder_type"); + auto extruders = printer_preset.config.option("extruder_type"); + auto nozzle_volumes_def = wxGetApp().preset_bundle->project_config.def()->get("nozzle_volume_type"); + auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); + auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); + auto diameter = printer_preset.config.opt_string("printer_variant"); auto extruder_max_nozzle_count = printer_preset.config.option("extruder_max_nozzle_count"); - auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, int index) { + auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants, + diameter, extruder_max_nozzle_count](ExtruderGroup& extruder, int index) { extruder.combo_flow->Clear(); - auto type = extruders_def->enum_labels[extruders->values[index]]; + auto type = extruders_def->enum_labels[extruders->values[index]]; int select = -1; for (size_t i = 0; i < nozzle_volumes_def->enum_labels.size(); ++i) { // get_at falls back to the first entry when a profile defines no per-extruder value, // so extruders without an explicit sub-nozzle count never offer Hybrid. A nullable-int // nil is INT_MAX (> 1) and would otherwise falsely pass the gate, so exclude it too. if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) || - extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && - nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid) { - if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" || - is_skip_high_flow_printer(printer_model))) + extruder_max_nozzle_count->get_at(index) > 1 && + extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && + nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid) { + if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow && + (diameter == "0.2" || is_skip_high_flow_printer(printer_model))) continue; if (nozzle_volumes->values[index] == i) select = extruder.combo_flow->GetCount(); - extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)i); + extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*) i); } } if (select == -1) @@ -3483,13 +3739,13 @@ void Sidebar::update_presets(Preset::Type preset_type) extruder.combo_flow->SetSelection(select); }; - auto update_extruder_diameter = [&diameters, &diameter, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { + auto update_extruder_diameter = [&diameters, &diameter, &nozzle_diameter](int extruder_index, ExtruderGroup& extruder) { extruder.combo_diameter->Clear(); int select = -1; // ORCA get the actual nozzle diameter from printer config auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias - if(diameters[0].empty() && !nozzle_dia.empty()){ + if (diameters[0].empty() && !nozzle_dia.empty()) { diameters[0] = nozzle_dia; } // Orca: Check if the actual nozzle diameter exists in the list, if not add it as a custom option @@ -3507,34 +3763,38 @@ void Sidebar::update_presets(Preset::Type preset_type) auto image_path = get_cur_select_bed_image(); if (is_dual_extruder) { std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); - p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); - p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); + p->left_extruder->SetTitle( + _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, + ToolHeadNameCase::TitleCase))); + p->right_extruder->SetTitle( + _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, + ToolHeadNameCase::TitleCase))); AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder); AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder); update_extruder_variant(*p->left_extruder, 0); update_extruder_variant(*p->right_extruder, 1); - //if (!p->is_switching_diameter) { - update_extruder_diameter(0, *p->left_extruder); - update_extruder_diameter(1, *p->right_extruder); + // if (!p->is_switching_diameter) { + update_extruder_diameter(0, *p->left_extruder); + update_extruder_diameter(1, *p->right_extruder); //} p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); } else { AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder); update_extruder_variant(*p->single_extruder, 0); - //if (!p->is_switching_diameter) - update_extruder_diameter(0, *p->single_extruder); + // if (!p->is_switching_diameter) + update_extruder_diameter(0, *p->single_extruder); // ORCA sync unified nozzle combo box p->combo_nozzle_dia->Clear(); for (size_t i = 0; i < diameters.size(); ++i) p->combo_nozzle_dia->Append(diameters[i], {}); p->combo_nozzle_dia->SetSelection((*p->single_extruder).combo_diameter->GetSelection()); - + // ORCA update nozzle type - const auto& full_config = wxGetApp().preset_bundle->full_config(); - wxString nozzle_type = "-"; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + wxString nozzle_type = "-"; const ConfigOptionEnumsGenericNullable* cfg_nozzle_type = full_config.option("nozzle_type"); - if(cfg_nozzle_type != nullptr){ + if (cfg_nozzle_type != nullptr) { std::vector nozzle_types(cfg_nozzle_type->size()); for (size_t idx = 0; idx < cfg_nozzle_type->size(); ++idx) nozzle_types[idx] = NozzleType(cfg_nozzle_type->values[idx]); @@ -3542,8 +3802,8 @@ void Sidebar::update_presets(Preset::Type preset_type) nozzle_types[0] == ntHardenedSteel ? "Hardened Steel" : nozzle_types[0] == ntStainlessSteel ? "Stainless Steel" : nozzle_types[0] == ntTungstenCarbide ? "Tungsten Carbide" : - nozzle_types[0] == ntBrass ? "Brass" - : "-" // Undefined + nozzle_types[0] == ntBrass ? "Brass" : + "-" // Undefined ); } p->label_nozzle_type->SetLabel(nozzle_type); @@ -3569,19 +3829,17 @@ void Sidebar::update_presets(Preset::Type preset_type) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": exit."); } -//BBS +// BBS void Sidebar::update_presets_from_to(Slic3r::Preset::Type preset_type, std::string from, std::string to) { - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%, from %2% to %3%")%preset_type %from %to; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%, from %2% to %3%") % preset_type % from % to; switch (preset_type) { - case Preset::TYPE_FILAMENT: - { + case Preset::TYPE_FILAMENT: { const size_t filament_cnt = p->combos_filament.size(); - for (auto it = preset_bundle.filament_presets.begin(); it != preset_bundle.filament_presets.end(); it++) - { + for (auto it = preset_bundle.filament_presets.begin(); it != preset_bundle.filament_presets.end(); it++) { if ((*it).compare(from) == 0) { (*it) = to; } @@ -3600,7 +3858,8 @@ void Sidebar::update_presets_from_to(Slic3r::Preset::Type preset_type, std::stri BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": exit!"); } -BedType Sidebar::get_cur_select_bed_type() { +BedType Sidebar::get_cur_select_bed_type() +{ int selection = p->combo_printer_bed->GetSelection(); if (selection < 0 && selection >= m_cur_combox_bed_types.size()) { p->combo_printer_bed->SetSelection(0); @@ -3612,13 +3871,14 @@ BedType Sidebar::get_cur_select_bed_type() { std::string Sidebar::get_cur_select_bed_image() { - auto select_bed_type = get_cur_select_bed_type(); - //auto series_suffix_str = m_cur_image_bed_type.empty() ? "" : ("_" + m_cur_image_bed_type); - auto image_path = bed_type_thumbnails[select_bed_type];// + series_suffix_str; + auto select_bed_type = get_cur_select_bed_type(); + // auto series_suffix_str = m_cur_image_bed_type.empty() ? "" : ("_" + m_cur_image_bed_type); + auto image_path = bed_type_thumbnails[select_bed_type]; // + series_suffix_str; return image_path; } -void Sidebar::set_bed_type_accord_combox(BedType bed_type) { +void Sidebar::set_bed_type_accord_combox(BedType bed_type) +{ for (size_t i = 0; i < m_cur_combox_bed_types.size(); i++) { if (m_cur_combox_bed_types[i] == bed_type) { p->combo_printer_bed->SelectAndNotify(i); @@ -3642,28 +3902,28 @@ bool Sidebar::reset_bed_type_combox_choices(bool is_sidebar_init) } if (m_last_combo_bedtype_count != 0 && pm) { auto cur_count = (int) BedType::btCount - 1 - pm->not_support_bed_types.size(); - if (cur_count == m_last_combo_bedtype_count) {//no change + if (cur_count == m_last_combo_bedtype_count) { // no change return false; } } - const ConfigOptionDef *bed_type_def = print_config_def.get("curr_bed_type"); + const ConfigOptionDef* bed_type_def = print_config_def.get("curr_bed_type"); p->combo_printer_bed->Clear(); m_cur_combox_bed_types.clear(); - if (pm &&bed_type_def && bed_type_def->enum_keys_map) { + if (pm && bed_type_def && bed_type_def->enum_keys_map) { int index = 0; for (auto item : bed_type_def->enum_labels) { index++; - bool find = std::find(pm->not_support_bed_types.begin(), pm->not_support_bed_types.end(), item) != pm->not_support_bed_types.end(); + bool find = std::find(pm->not_support_bed_types.begin(), pm->not_support_bed_types.end(), item) != + pm->not_support_bed_types.end(); if (find) { continue; } - m_cur_combox_bed_types.emplace_back(BedType(index));//BedType //btPC =1 + m_cur_combox_bed_types.emplace_back(BedType(index)); // BedType //btPC =1 p->combo_printer_bed->AppendString(_L(item)); } - } - else { + } else { m_cur_image_bed_type = ""; - int index = 0; + int index = 0; for (auto item : bed_type_def->enum_labels) { index++; m_cur_combox_bed_types.emplace_back(BedType(index)); // BedType //btPC =1 @@ -3688,13 +3948,1157 @@ void Sidebar::change_top_border_for_mode_sizer(bool increase_border) #endif } +// ---- Mixed-color filament sidebar support ---- +// The mixed rows get their own scroll area, capped by Orca's filaments_area_preferred_count +// row budget rather than BBS's fixed 3-row / 12-filament limit. +void Sidebar::recalc_filament_scroll_sizes() +{ + if (!p->m_mixed_scroll_area || !p->m_mixed_scroll_area->GetSizer()) + return; + + // Same preferred-row budget the physical list uses, so both lists cap consistently. + auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); + const int row_h = combo_sizer ? combo_sizer->GetSize().GetHeight() : 0; + int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); + const int max_h = (row_h > 0) ? preferred_rows * row_h : -1; + + auto content_size = p->m_mixed_scroll_area->GetSizer()->GetMinSize(); + if (max_h > 0 && content_size.y > max_h) { + p->m_mixed_scroll_area->SetMaxSize({-1, max_h}); + content_size.y = max_h; + } else { + p->m_mixed_scroll_area->SetMaxSize({-1, -1}); + } + p->m_mixed_scroll_area->SetMinSize({0, content_size.y}); +} +static std::string blend_mixed_color(const std::vector& comp_ids, + const std::vector& ratios, + const std::vector& color_strs) +{ + std::vector hex_colors; + hex_colors.reserve(comp_ids.size()); + for (unsigned int id : comp_ids) + hex_colors.push_back((id >= 1 && id <= color_strs.size()) ? color_strs[id - 1] : "#808080"); + return Slic3r::blend_color_multi(hex_colors, ratios); +} + +void Sidebar::update_mixed_filament_list() +{ + auto* plater = dynamic_cast(GetParent()); + if (!plater) + return; + + wxWindowUpdateLocker noUpdates(this); + + const wxColour mc_bg = StateColor::darkModeColorFor(*wxWHITE); + const wxColour mc_border = StateColor::darkModeColorFor(wxColour("#CECECE")); + const wxColour mc_text = StateColor::darkModeColorFor(wxColour("#262E30")); + const wxColour mc_dim = StateColor::darkModeColorFor(wxColour("#ACACAC")); + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto mixed_indices = plater->mixed_filament_config_indices(); + size_t num_physical = p->combos_filament.size(); + + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* components_opt = project_config.option("filament_mixed_components"); + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + auto* colours_opt = project_config.option("filament_colour"); + auto* grad_opt = project_config.option("filament_mixed_gradient"); + auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); + + bool can_mix = (num_physical >= 2); + bool has_mixed = can_mix && !mixed_indices.empty(); + + // Check integrity of mixed filament component references + std::vector broken_slots; + if (is_mixed_opt && components_opt) + broken_slots = check_mixed_filament_integrity(is_mixed_opt->values, components_opt->values, num_physical); + std::set broken_set(broken_slots.begin(), broken_slots.end()); + + // Type consistency check + if (is_mixed_opt && components_opt) { + std::vector physical_types; + auto& preset_bundle = *wxGetApp().preset_bundle; + for (size_t i = 0; i < num_physical; ++i) { + std::string ft; + if (i < preset_bundle.filament_presets.size()) { + auto* preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[i]); + if (preset) { + std::string display_type; + ft = preset->config.get_filament_type(display_type); + } + } + if (ft.empty()) + ft = "PLA"; + physical_types.push_back(ft); + } + auto type_mismatch_slots = check_mixed_filament_type_consistency(is_mixed_opt->values, components_opt->values, physical_types); + for (size_t s : type_mismatch_slots) + broken_set.insert(s); + broken_slots.insert(broken_slots.end(), type_mismatch_slots.begin(), type_mismatch_slots.end()); + } + + bool at_limit = (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)); + p->m_btn_add_mixed_filament->Show(can_mix && !has_mixed && !at_limit); + p->m_panel_mixed_title->Show(has_mixed); + p->m_mixed_scroll_area->Show(has_mixed); + p->m_panel_mixed_content->Show(has_mixed); + if (p->m_btn_mixed_add) + p->m_btn_mixed_add->Enable(!at_limit); + p->m_panel_mixed_warning->Show(false); + + // Show/dismiss 3D canvas notification for broken mixed filaments + if (has_mixed && !broken_set.empty()) { + auto* notify = wxGetApp().plater()->get_notification_manager(); + if (notify) + notify->push_notification(NotificationType::BBLMixedFilamentBroken, + NotificationManager::NotificationLevel::ErrorNotificationLevel, + _u8L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); + } else { + auto* notify = wxGetApp().plater()->get_notification_manager(); + if (notify) + notify->close_notification_of_type(NotificationType::BBLMixedFilamentBroken); + } + + if (has_mixed) { + auto* left_col = p->m_sizer_mixed_filaments->GetItem(size_t(0))->GetSizer(); + auto* right_col = p->m_sizer_mixed_filaments->GetItem(size_t(1))->GetSizer(); + left_col->Clear(true); + right_col->Clear(true); + + std::vector physical_colors; + if (colours_opt) { + for (size_t i = 0; i < num_physical && i < colours_opt->values.size(); ++i) + physical_colors.push_back(colours_opt->values[i]); + } + + auto make_swatch_panel = [this](wxWindow* parent, const wxColour& col, unsigned int num) -> wxPanel* { + int swatch_sz = FromDIP(20); + auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); + panel->SetMinSize(wxSize(swatch_sz, swatch_sz)); + bool is_dark = wxGetApp().dark_mode(); + panel->Bind(wxEVT_PAINT, [panel, col, num, is_dark](wxPaintEvent&) { + wxPaintDC dc(panel); + wxSize sz = panel->GetClientSize(); + dc.SetBackground(wxBrush(col)); + dc.Clear(); + if (!is_dark && col.Red() > 224 && col.Green() > 224 && col.Blue() > 224) { + dc.SetPen(wxPen(wxColour(130, 130, 128), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + } + if (is_dark && col.Red() < 45 && col.Green() < 45 && col.Blue() < 45) { + dc.SetPen(wxPen(wxColour(207, 207, 207), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + } + wxString txt = wxString::Format("%u", num); + dc.SetFont(::Label::Body_14); + wxSize txt_sz = dc.GetTextExtent(txt); + dc.SetTextForeground(col.GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); + dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, (sz.GetHeight() - txt_sz.GetHeight()) / 2); + }); + return panel; + }; + + for (size_t i = 0; i < mixed_indices.size(); ++i) { + size_t cfg_idx = mixed_indices[i]; + auto* combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + combo_and_btn_sizer->Add(FromDIP(10), 0, 0, 0, 0); + + // Parse components and ratios from config strings (supports 2-N components) + std::vector comp_ids; + std::vector comp_ratios; + if (components_opt && cfg_idx < components_opt->values.size()) { + std::istringstream iss(components_opt->values[cfg_idx]); + std::string tok; + while (std::getline(iss, tok, ',')) { + unsigned int v = 0; + if (std::sscanf(tok.c_str(), "%u", &v) == 1) + comp_ids.push_back(v); + } + } + if (ratios_opt && cfg_idx < ratios_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + std::istringstream iss(ratios_opt->values[cfg_idx]); + std::string tok; + while (std::getline(iss, tok, ',')) { + float v = 0; + if (std::sscanf(tok.c_str(), "%f", &v) == 1) + comp_ratios.push_back((int) (v * 100 + 0.5f)); + } + } + if (!comp_ids.empty() && comp_ratios.size() != comp_ids.size()) { + BOOST_LOG_TRIVIAL(warning) << "Mixed filament slot " << cfg_idx << ": ratio count (" << comp_ratios.size() + << ") != component count (" << comp_ids.size() << "), resetting to even distribution"; + int n = (int) comp_ids.size(); + comp_ratios.assign(n, 100 / n); + comp_ratios[0] += 100 - (100 / n) * n; + } + + bool is_broken = broken_set.count(cfg_idx) > 0; + + // Recalculate mixed color based on current physical colors + if (!is_broken && !comp_ids.empty() && comp_ids.size() == comp_ratios.size()) { + std::string new_mixed_color = blend_mixed_color(comp_ids, comp_ratios, physical_colors); + + if (colours_opt && cfg_idx < colours_opt->values.size() && colours_opt->values[cfg_idx] != new_mixed_color) { + colours_opt->values[cfg_idx] = new_mixed_color; + + auto* multi_colour_opt = project_config.option("filament_multi_colour"); + if (multi_colour_opt && cfg_idx < multi_colour_opt->values.size()) { + multi_colour_opt->values[cfg_idx] = new_mixed_color; + } + } + } + + bool is_gradient = false; + int gradient_direction = 0; + if (grad_opt && cfg_idx < grad_opt->values.size()) + is_gradient = grad_opt->values[cfg_idx]; + if (is_gradient && grad_range_opt && cfg_idx < grad_range_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(grad_range_opt->values[cfg_idx].c_str(), "%f,%f", &v0, &v1) == 2) + gradient_direction = (v0 > v1) ? 0 : 1; + } + + std::string mix_color_str = (colours_opt && cfg_idx < colours_opt->values.size()) ? colours_opt->values[cfg_idx] : "#888888"; + wxColour mix_col(mix_color_str); + unsigned int mix_num = (unsigned int) (cfg_idx + 1); + + // The swatch fades bottom to top over the model's height, sampled the same way + // the slicer builds the sublayers, so it matches the editor's Effect Preview. The + // ramp comes back empty for every slot that is not a two component gradient mix. + const int swatch_sz = FromDIP(20); + const std::vector gradient_ramp = mixed_gradient_ramp(project_config, cfg_idx, swatch_sz); + + if (!gradient_ramp.empty()) { + auto* grad_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY, wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); + grad_panel->SetMinSize(wxSize(swatch_sz, swatch_sz)); + grad_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + grad_panel->Bind(wxEVT_PAINT, [grad_panel, gradient_ramp, mix_num](wxPaintEvent&) { + wxBufferedPaintDC dc(grad_panel); + wxSize sz = grad_panel->GetClientSize(); + fill_gradient_ramp_rect(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()), gradient_ramp); + wxString txt = wxString::Format("%u", mix_num); + dc.SetFont(::Label::Body_14); + wxSize txt_sz = dc.GetTextExtent(txt); + // The number sits at the swatch's middle, so take its contrast from the + // colour printed at mid height rather than from either endpoint. + dc.SetTextForeground(gradient_ramp[gradient_ramp.size() / 2].GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); + dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, (sz.GetHeight() - txt_sz.GetHeight()) / 2); + }); + combo_and_btn_sizer->Add(grad_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + } else { + combo_and_btn_sizer->Add(make_swatch_panel(p->m_panel_mixed_content, mix_col, mix_num), 0, + wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + } + + auto* content_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY); + content_panel->SetBackgroundColour(mc_bg); + content_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + + // Pre-compute all values the paint lambda needs (avoid capturing `this` for FromDIP) + int cp_pad = FromDIP(4); + int cp_swatch_sz = FromDIP(20); + int cp_sep_margin = FromDIP(3); + int cp_pct_left = FromDIP(2); + int cp_gap = FromDIP(2); + int cp_pct_gap = FromDIP(4); + bool cp_is_dark = wxGetApp().dark_mode(); + + // Build per-component colour list for the lambda + std::vector cp_colours; + std::vector cp_valid; + std::vector cp_ids = comp_ids; + std::vector cp_ratios = comp_ratios; + bool cp_is_gradient = is_gradient; + int cp_gradient_dir = gradient_direction; + for (size_t ci = 0; ci < comp_ids.size(); ++ci) { + bool valid = (comp_ids[ci] >= 1 && comp_ids[ci] <= physical_colors.size()); + cp_valid.push_back(valid); + cp_colours.push_back(valid ? wxColour(physical_colors[comp_ids[ci] - 1]) : wxColour("#D9D9D9")); + } + + // Reorder for gradient display: from -> to + std::vector draw_ids; + std::vector draw_ratios; + std::vector draw_colours; + std::vector draw_valid; + if (cp_is_gradient && cp_ids.size() == 2) { + int fi = (cp_gradient_dir == 0) ? 0 : 1; + int ti = 1 - fi; + draw_ids = {cp_ids[fi], cp_ids[ti]}; + draw_ratios = {cp_ratios.size() > (size_t) fi ? cp_ratios[fi] : 0, cp_ratios.size() > (size_t) ti ? cp_ratios[ti] : 0}; + draw_colours = {cp_colours[fi], cp_colours[ti]}; + draw_valid = {cp_valid[fi], cp_valid[ti]}; + } else { + draw_ids = cp_ids; + draw_ratios = cp_ratios; + draw_colours = cp_colours; + draw_valid = cp_valid; + } + + content_panel->Bind(wxEVT_PAINT, [content_panel, mc_bg, mc_border, mc_text, mc_dim, cp_pad, cp_swatch_sz, cp_sep_margin, + cp_pct_left, cp_gap, cp_pct_gap, cp_is_dark, cp_is_gradient, draw_ids, draw_ratios, + draw_colours, draw_valid](wxPaintEvent&) { + wxBufferedPaintDC dc(content_panel); + wxSize sz = content_panel->GetClientSize(); + + dc.SetBrush(wxBrush(mc_bg)); + dc.SetPen(wxPen(mc_border, 1)); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + dc.SetFont(::Label::Body_13); + int x = cp_pad; + int y_swatch = (sz.GetHeight() - cp_swatch_sz) / 2; + int text_h = dc.GetTextExtent(wxT("A")).GetHeight(); + int y_text = y_swatch + (cp_swatch_sz - text_h) / 2; + int avail = sz.GetWidth() - cp_pad; + wxString ellipsis = wxT("..."); + int ellipsis_w = dc.GetTextExtent(ellipsis).GetWidth(); + + auto fits = [&](int needed) -> bool { return (x + needed) <= (avail - ellipsis_w); }; + + size_t n = draw_ids.size(); + for (size_t ci = 0; ci < n; ++ci) { + // Separator: "+" or arrow + if (ci > 0) { + wxString sep = cp_is_gradient ? wxT("\u2192") : wxT("+"); + int sep_w = dc.GetTextExtent(sep).GetWidth() + cp_sep_margin * 2; + if (!fits(sep_w + cp_swatch_sz)) { + dc.SetTextForeground(mc_text); + dc.DrawText(ellipsis, x, y_text); + break; + } + dc.SetTextForeground(mc_text); + dc.DrawText(sep, x + cp_sep_margin, y_text); + x += sep_w; + } + + // Swatch + if (!fits(cp_swatch_sz)) { + dc.SetTextForeground(mc_text); + dc.DrawText(ellipsis, x, y_text); + break; + } + + if (draw_valid[ci]) { + wxColour col = draw_colours[ci]; + dc.SetBrush(wxBrush(col)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + if (!cp_is_dark && col.Red() > 224 && col.Green() > 224 && col.Blue() > 224) { + dc.SetPen(wxPen(wxColour(130, 130, 128), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + } + if (cp_is_dark && col.Red() < 45 && col.Green() < 45 && col.Blue() < 45) { + dc.SetPen(wxPen(wxColour(207, 207, 207), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + } + dc.SetFont(::Label::Body_14); + wxString num = wxString::Format("%u", draw_ids[ci]); + wxSize num_sz = dc.GetTextExtent(num); + dc.SetTextForeground(col.GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); + dc.DrawText(num, x + (cp_swatch_sz - num_sz.GetWidth()) / 2, y_swatch + (cp_swatch_sz - num_sz.GetHeight()) / 2); + dc.SetFont(::Label::Body_13); + } else { + dc.SetBrush(wxBrush(mc_bg)); + dc.SetPen(wxPen(mc_dim, 1)); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + wxString dash = wxT("\u2014"); + wxSize dash_sz = dc.GetTextExtent(dash); + dc.SetTextForeground(mc_dim); + dc.DrawText(dash, x + (cp_swatch_sz - dash_sz.GetWidth()) / 2, y_swatch + (cp_swatch_sz - dash_sz.GetHeight()) / 2); + } + x += cp_swatch_sz + cp_gap; + + // Ratio text (skip for gradient) + if (!cp_is_gradient) { + int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; + wxString pct = wxString::Format("%d%%", r); + int pct_w = dc.GetTextExtent(pct).GetWidth(); + if (!fits(pct_w)) { + dc.SetTextForeground(mc_text); + dc.DrawText(ellipsis, x, y_text); + break; + } + dc.SetTextForeground(mc_text); + dc.DrawText(pct, x + cp_pct_left, y_text); + x += pct_w + cp_pct_gap; + } + } + }); + + // Tooltip: always show full info + { + wxString tip; + for (size_t ci = 0; ci < draw_ids.size(); ++ci) { + if (ci > 0) + tip += cp_is_gradient ? wxT(" \u2192 ") : wxT(" + "); + int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; + tip += wxString::Format("%u (%d%%)", draw_ids[ci], r); + } + content_panel->SetToolTip(tip); + } + + // Repaint on resize so truncation updates + content_panel->Bind(wxEVT_SIZE, [content_panel](wxSizeEvent& e) { + content_panel->Refresh(); + e.Skip(); + }); + + content_panel->SetCursor(wxCursor(wxCURSOR_HAND)); + size_t panel_idx = i; + content_panel->Bind(wxEVT_LEFT_UP, [this, panel_idx](wxMouseEvent&) { edit_mixed_filament(panel_idx); }); + + combo_and_btn_sizer->Add(content_panel, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, FromDIP(30)}); + + auto* menu_btn = new ScalableButton(p->m_panel_mixed_content, wxID_ANY, is_broken ? "error" : "menu_filament"); + menu_btn->SetToolTip(is_broken ? _L("Mixed filament has broken component references") : _L("Edit / Delete / Merge")); + menu_btn->Bind(wxEVT_BUTTON, [this, panel_idx, cfg_idx](wxCommandEvent&) { + wxMenu menu; + + auto* edit_item = menu.Append(wxID_ANY, _L("Edit")); + menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { edit_mixed_filament(panel_idx); }, edit_item->GetId()); + + wxMenu* sub_menu = new wxMenu(); + std::vector icons = get_extruder_color_icons(true); + int filaments_cnt = icons.size(); + for (int j = 0; j < filaments_cnt; ++j) { + if ((size_t) j == cfg_idx) + continue; + + wxString item_name; + bool is_target_mixed = wxGetApp().preset_bundle->is_mixed_filament(j); + if (is_target_mixed) { + item_name = wxString::Format(_L("Filament %d"), j + 1); + } else { + auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[j]); + item_name = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), j + 1); + } + + auto* mi = new wxMenuItem(sub_menu, wxID_ANY, item_name); +#ifndef __linux__ + mi->SetBitmap(*icons[j]); +#endif + sub_menu->Append(mi); + sub_menu->Bind(wxEVT_MENU, [this, cfg_idx, j](wxCommandEvent&) { change_filament(cfg_idx, j); }, mi->GetId()); + } + if (filaments_cnt > 1) + menu.AppendSubMenu(sub_menu, _L("Merge with")); + else + delete sub_menu; + + menu.AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete + + // ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS + auto* del_item = menu.Append(wxID_ANY, _L("Delete")); + menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { delete_mixed_filament_at(panel_idx); }, del_item->GetId()); + + PopupMenu(&menu); + }); + combo_and_btn_sizer->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + + combo_and_btn_sizer->Add(FromDIP(16), 0, 0, 0, 0); + + int side = i % 2; + auto* col = (side == 0) ? left_col : right_col; + if (side == 1 && i > 1) + col->Remove(i / 2); + col->Add(combo_and_btn_sizer, 1, wxEXPAND); + if (side == 0 && i > 0) { + right_col->AddStretchSpacer(1); + } + } + } + + recalc_filament_scroll_sizes(); + + p->m_panel_filament_content->FitInside(); + p->m_mixed_scroll_area->FitInside(); + p->scrolled->Layout(); + m_scrolled_sizer->Layout(); + p->scrolled->Layout(); + + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + obj_list()->update_objects_list_filament_column(total); + + // Sync mixed filament colors into the config used by 3D view rendering. + plater->update_filament_colors_in_full_config(); + obj_list()->update_filament_colors(); + + // Check if any broken mixed filament is used by objects on current plate. + // Scan raw extruder assignments (object / volume / height-range / painting) + // instead of get_extruders() which expands mixed slots and loses their IDs. + p->m_mixed_filament_broken = false; + if (!broken_slots.empty()) { + std::set broken_1based; + for (size_t s : broken_slots) + broken_1based.insert(s + 1); + + auto* curr_plate = plater->get_partplate_list().get_curr_plate(); + if (curr_plate) { + for (auto& obj : plater->model().objects) { + if (!curr_plate->contain_instance_totally(obj, 0)) + continue; + // Check object-level extruder + int obj_ext = obj->config.has("extruder") ? obj->config.extruder() : 1; + if (broken_1based.count((size_t) obj_ext)) { + p->m_mixed_filament_broken = true; + break; + } + bool found = false; + for (auto* vol : obj->volumes) { + // Check volume-level extruder + int vol_ext = vol->config.has("extruder") ? vol->config.extruder() : obj_ext; + if (broken_1based.count((size_t) vol_ext)) { + found = true; + break; + } + // Check color painting data (mmu segmentation facets) + if (vol->is_model_part() && !vol->mmu_segmentation_facets.empty()) { + for (size_t broken_slot : broken_1based) { + if (vol->mmu_segmentation_facets.has_facets(*vol, EnforcerBlockerType(broken_slot))) { + found = true; + break; + } + } + if (found) + break; + } + } + if (found) { + p->m_mixed_filament_broken = true; + break; + } + // Check height range modifier extruder overrides + for (auto& [range, cfg] : obj->layer_config_ranges) { + if (cfg.has("extruder")) { + int layer_ext = cfg.option("extruder")->getInt(); + if (layer_ext > 0 && broken_1based.count((size_t) layer_ext)) { + found = true; + break; + } + } + } + if (found) { + p->m_mixed_filament_broken = true; + break; + } + } + } + } + + if (plater->canvas3D()) { + plater->canvas3D()->set_as_dirty(); + plater->get_view3D_canvas3D()->reload_scene(false); + } + + if (p->m_mixed_filament_broken) { + auto* mf = wxGetApp().mainframe; + if (mf) + mf->update_slice_print_status(MainFrame::eEventObjectUpdate, false); + } + + if (auto* tab = dynamic_cast(wxGetApp().plate_tab)) + tab->update_mixed_filament_seq_state(); +} + +bool Sidebar::has_broken_mixed_filament() const +{ + auto* plater = dynamic_cast(GetParent()); + if (!plater) + return false; + return has_broken_mixed_filament(plater->get_partplate_list().get_curr_plate()); +} + +bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const +{ + if (!plate) + return false; + auto* plater = dynamic_cast(GetParent()); + if (!plater) + return false; + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* comp_strs_opt = project_config.option("filament_mixed_components"); + if (!is_mixed_opt || !comp_strs_opt) + return false; + + size_t num_physical = p->combos_filament.size(); + auto broken_slots = check_mixed_filament_integrity(is_mixed_opt->values, comp_strs_opt->values, num_physical); + + // Type consistency check + { + std::vector physical_types; + auto& preset_bundle = *wxGetApp().preset_bundle; + for (size_t i = 0; i < num_physical; ++i) { + std::string ft; + if (i < preset_bundle.filament_presets.size()) { + auto* preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[i]); + if (preset) { + std::string display_type; + ft = preset->config.get_filament_type(display_type); + } + } + if (ft.empty()) + ft = "PLA"; + physical_types.push_back(ft); + } + auto type_mismatch_slots = check_mixed_filament_type_consistency(is_mixed_opt->values, comp_strs_opt->values, physical_types); + broken_slots.insert(broken_slots.end(), type_mismatch_slots.begin(), type_mismatch_slots.end()); + } + + if (broken_slots.empty()) + return false; + + std::set broken_1based; + for (size_t s : broken_slots) + broken_1based.insert(s + 1); + + // Scan model objects on the given plate for raw extruder assignments + // (don't use get_extruders() which expands mixed slots) + for (auto& entry : plater->model().objects) { + if (!plate->contain_instance_totally(entry, 0)) + continue; + // Check object-level extruder + int obj_ext = entry->config.has("extruder") ? entry->config.extruder() : 1; + if (broken_1based.count((size_t) obj_ext)) + return true; + for (auto* vol : entry->volumes) { + // Check volume-level extruder + int vol_ext = vol->config.has("extruder") ? vol->config.extruder() : obj_ext; + if (broken_1based.count((size_t) vol_ext)) + return true; + // Check color painting data (mmu segmentation facets) + if (vol->is_model_part() && !vol->mmu_segmentation_facets.empty()) { + for (size_t broken_slot : broken_1based) { + if (vol->mmu_segmentation_facets.has_facets(*vol, EnforcerBlockerType(broken_slot))) + return true; + } + } + } + // Check height range modifier extruder overrides + for (auto& [range, cfg] : entry->layer_config_ranges) { + if (cfg.has("extruder")) { + int layer_ext = cfg.option("extruder")->getInt(); + if (layer_ext > 0 && broken_1based.count((size_t) layer_ext)) + return true; + } + } + } + + return false; +} + +void Sidebar::collect_physical_filament_info(std::vector& color_strs, + std::vector& names, + std::vector& types, + std::vector* config_indices) +{ + color_strs.clear(); + names.clear(); + types.clear(); + if (config_indices) + config_indices->clear(); + + size_t num_physical = p->combos_filament.size(); + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + std::vector physical_indices; + const size_t total = wxGetApp().preset_bundle->filament_presets.size(); + physical_indices.reserve(num_physical); + for (size_t i = 0; i < total && physical_indices.size() < num_physical; ++i) { + if (!is_mixed_opt || i >= is_mixed_opt->values.size() || !is_mixed_opt->values[i]) + physical_indices.push_back(i); + } + while (physical_indices.size() < num_physical) + physical_indices.push_back(physical_indices.size()); + if (config_indices) + *config_indices = physical_indices; + + auto* colours_opt = project_config.option("filament_colour"); + if (colours_opt) { + for (size_t i = 0; i < num_physical; ++i) { + const size_t cfg_idx = physical_indices[i]; + if (cfg_idx < colours_opt->values.size()) + color_strs.push_back(colours_opt->values[cfg_idx]); + } + } + + for (size_t i = 0; i < num_physical; ++i) { + auto* combo = p->combos_filament[i]; + names.push_back(combo ? into_u8(combo->GetValue()) : "Filament " + std::to_string(i + 1)); + } + + auto& preset_bundle = *wxGetApp().preset_bundle; + for (size_t i = 0; i < num_physical; ++i) { + const size_t cfg_idx = physical_indices[i]; + Preset* preset = nullptr; + if (cfg_idx < preset_bundle.filament_presets.size()) + preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]); + std::string ft; + if (preset) { + std::string display_type; + ft = preset->config.get_filament_type(display_type); + } + if (ft.empty()) + ft = "PLA"; + types.push_back(ft); + } +} + +// Serialize the dialog's custom gradient curve only when it deviates from the +// direction-implied two-point linear default. Returning an empty string keeps +// projects with the default shape bit-identical with the legacy 2-field format +// (curve string stays "" so the slicer falls back to gradient_range linear). +// Shared by add_mixed_filament / edit_mixed_filament so the "is default" rule +// stays consistent between both entry points. +static std::string serialize_mixed_gradient_curve_if_custom(const MixedFilamentResult& result) +{ + if (!(result.components.size() == 2 && !result.gradient_curve.empty())) + return {}; + + const double y0 = (result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; + const double y1 = (result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; + const double eps = 1e-4; + if (result.gradient_curve.size() == 2) { + const auto& a0 = result.gradient_curve[0]; + const auto& a1 = result.gradient_curve[1]; + // Default curve also requires no tangent overrides; any finite tangent + // means the user bent the segment, so we must serialize it. + const bool is_default = std::abs(a0.x - 0.0) < eps && std::abs(a1.x - 1.0) < eps && std::abs(a0.y - y0) < eps && + std::abs(a1.y - y1) < eps && !std::isfinite(a0.m_in) && !std::isfinite(a0.m_out) && + !std::isfinite(a1.m_in) && !std::isfinite(a1.m_out); + if (is_default) + return {}; + } + + Slic3r::GradientCurve gc; + gc.points = result.gradient_curve; + return Slic3r::serialize_gradient_curve(gc); +} + +static bool create_mixed_filament_from_result(Sidebar* sidebar, + const MixedFilamentResult& result, + const std::vector& color_strs) +{ + if (!sidebar || result.components.size() < 2 || result.ratios.size() < 2) + return false; + if (!dynamic_cast(sidebar->GetParent())) + return false; + + size_t num_physical = sidebar->combos_filament().size(); + if (num_physical < 2) + return false; + if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) + return false; + + auto& project_config = wxGetApp().preset_bundle->project_config; + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + size_t new_idx = total; + + std::string mixed_color = blend_mixed_color(result.components, result.ratios, color_strs); + wxGetApp().preset_bundle->set_num_filaments(total + 1, mixed_color); + + auto* multi_colour_opt = project_config.option("filament_multi_colour"); + if (multi_colour_opt) { + while (multi_colour_opt->values.size() <= new_idx) + multi_colour_opt->values.push_back(""); + multi_colour_opt->values[new_idx] = mixed_color; + } + + // set_num_filaments() above already grows these parallel arrays; the writes are still + // size-guarded so a sizing bug degrades into a no-op rather than a heap overwrite. + { + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + while (is_mixed_opt->values.size() <= new_idx) + is_mixed_opt->values.push_back(false); + is_mixed_opt->values[new_idx] = true; + } + + std::string comp_str; + for (size_t i = 0; i < result.components.size(); ++i) { + if (i > 0) + comp_str += ","; + comp_str += std::to_string(result.components[i]); + } + { + auto* comp_opt = project_config.option("filament_mixed_components"); + while (comp_opt->values.size() <= new_idx) + comp_opt->values.push_back(std::string{}); + comp_opt->values[new_idx] = comp_str; + } + + int ratio_sum = 0; + for (int r : result.ratios) + ratio_sum += r; + if (ratio_sum <= 0) + ratio_sum = 100; + + std::string ratio_str; + { + CNumericLocalesSetter c_locale_setter; + for (size_t i = 0; i < result.ratios.size(); ++i) { + if (i > 0) + ratio_str += ","; + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.4f", (float) result.ratios[i] / ratio_sum); + ratio_str += buf; + } + } + { + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + while (ratios_opt->values.size() <= new_idx) + ratios_opt->values.push_back(std::string{}); + ratios_opt->values[new_idx] = ratio_str; + } + + if (!project_config.option("filament_mixed_gradient")) + project_config.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false})); + if (!project_config.option("filament_mixed_gradient_range")) + project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""})); + if (!project_config.option("filament_mixed_gradient_curve")) + project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""})); + if (!project_config.option("filament_mixed_gradient_per_part")) + project_config.set_key_value("filament_mixed_gradient_per_part", new ConfigOptionBools({false})); + + { + auto* grad_opt = project_config.option("filament_mixed_gradient"); + while (grad_opt->values.size() <= new_idx) + grad_opt->values.push_back(false); + grad_opt->values[new_idx] = result.gradient_enabled; + } + { + auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); + while (grad_range_opt->values.size() <= new_idx) + grad_range_opt->values.push_back(""); + if (result.gradient_enabled && result.components.size() == 2) { + const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; + grad_range_opt->values[new_idx] = fmt; + } else { + grad_range_opt->values[new_idx] = ""; + } + } + { + auto* grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); + while (grad_curve_opt->values.size() <= new_idx) + grad_curve_opt->values.push_back(""); + grad_curve_opt->values[new_idx] = serialize_mixed_gradient_curve_if_custom(result); + } + { + auto* per_part_opt = project_config.option("filament_mixed_gradient_per_part"); + while (per_part_opt->values.size() <= new_idx) + per_part_opt->values.push_back(false); + per_part_opt->values[new_idx] = result.gradient_enabled && result.per_part_gradient; + } + + auto& presets = wxGetApp().preset_bundle->filament_presets; + if (result.components[0] >= 1 && result.components[0] <= num_physical && presets.size() > new_idx) + presets[new_idx] = presets[result.components[0] - 1]; + + size_t filament_count = wxGetApp().preset_bundle->filament_presets.size(); + wxGetApp().plater()->get_partplate_list().on_filament_added(filament_count); + wxGetApp().plater()->on_filament_count_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + sidebar->update_mixed_filament_list(); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(sidebar, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, sidebar)); + return true; +} + +void Sidebar::add_mixed_filament() +{ + auto* plater = dynamic_cast(GetParent()); + if (!plater) + return; + + size_t num_physical = p->combos_filament.size(); + if (num_physical < 2) + return; + if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) + return; + + std::vector color_strs, names, types; + collect_physical_filament_info(color_strs, names, types); + + MixedFilamentDialog dlg(this, color_strs, names, types); + if (dlg.ShowModal() == wxID_OK) { + auto result = dlg.get_result(); + create_mixed_filament_from_result(this, result, color_strs); + } +} + +void Sidebar::edit_mixed_filament(size_t panel_idx) +{ + auto* plater = dynamic_cast(GetParent()); + if (!plater) + return; + + auto mixed_indices = plater->mixed_filament_config_indices(); + if (panel_idx >= mixed_indices.size()) + return; + size_t cfg_idx = mixed_indices[panel_idx]; + + std::vector color_strs, names, types; + collect_physical_filament_info(color_strs, names, types); + + auto& project_config = wxGetApp().preset_bundle->project_config; + MixedFilamentResult existing; + auto* components_opt = project_config.option("filament_mixed_components"); + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + + // Parse existing components + if (components_opt && cfg_idx < components_opt->values.size()) { + const std::string& cs = components_opt->values[cfg_idx]; + std::istringstream iss(cs); + std::string tok; + while (std::getline(iss, tok, ',')) { + unsigned int v = 0; + if (std::sscanf(tok.c_str(), "%u", &v) == 1) + existing.components.push_back(v); + } + } + // Parse existing ratios + if (ratios_opt && cfg_idx < ratios_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + const std::string& rs = ratios_opt->values[cfg_idx]; + std::istringstream iss(rs); + std::string tok; + while (std::getline(iss, tok, ',')) { + float v = 0; + if (std::sscanf(tok.c_str(), "%f", &v) == 1) + existing.ratios.push_back((int) (v * 100 + 0.5f)); + } + } + if (existing.components.size() < 2) { + existing.components = {1, 2}; + existing.ratios = {50, 50}; + } else if (existing.ratios.size() != existing.components.size()) { + BOOST_LOG_TRIVIAL(warning) << "Mixed filament edit: ratio count (" << existing.ratios.size() << ") != component count (" + << existing.components.size() << "), resetting to even distribution"; + int n = (int) existing.components.size(); + existing.ratios.assign(n, 100 / n); + existing.ratios[0] += 100 - (100 / n) * n; + } + + // Read gradient settings + auto* grad_opt = project_config.option("filament_mixed_gradient"); + if (grad_opt && cfg_idx < grad_opt->values.size()) + existing.gradient_enabled = grad_opt->values[cfg_idx]; + auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); + if (existing.gradient_enabled && grad_range_opt && cfg_idx < grad_range_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(grad_range_opt->values[cfg_idx].c_str(), "%f,%f", &v0, &v1) == 2) + existing.gradient_direction = (v0 > v1) ? 0 : 1; + } + auto* grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); + if (existing.gradient_enabled && grad_curve_opt && cfg_idx < grad_curve_opt->values.size()) { + auto curve = Slic3r::parse_gradient_curve(grad_curve_opt->values[cfg_idx]); + existing.gradient_curve = curve.points; + } + auto* per_part_opt = project_config.option("filament_mixed_gradient_per_part"); + if (existing.gradient_enabled && per_part_opt && cfg_idx < per_part_opt->values.size()) + existing.per_part_gradient = per_part_opt->values[cfg_idx]; + + MixedFilamentDialog dlg(this, existing, color_strs, names, types); + if (dlg.ShowModal() == wxID_OK) { + auto result = dlg.get_result(); + if (result.components.size() < 2 || result.ratios.size() < 2) + return; + + // Serialize components + std::string comp_str; + for (size_t i = 0; i < result.components.size(); ++i) { + if (i > 0) + comp_str += ","; + comp_str += std::to_string(result.components[i]); + } + components_opt->values[cfg_idx] = comp_str; + + // Serialize ratios + int ratio_sum = 0; + for (int r : result.ratios) + ratio_sum += r; + if (ratio_sum <= 0) + ratio_sum = 100; + + std::string ratio_str; + { + CNumericLocalesSetter c_locale_setter; + for (size_t i = 0; i < result.ratios.size(); ++i) { + if (i > 0) + ratio_str += ","; + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.4f", (float) result.ratios[i] / ratio_sum); + ratio_str += buf; + } + } + ratios_opt->values[cfg_idx] = ratio_str; + + // Gradient settings — ensure keys exist in dynamic config + if (!project_config.option("filament_mixed_gradient")) + project_config.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false})); + if (!project_config.option("filament_mixed_gradient_range")) + project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""})); + if (!project_config.option("filament_mixed_gradient_curve")) + project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""})); + if (!project_config.option("filament_mixed_gradient_per_part")) + project_config.set_key_value("filament_mixed_gradient_per_part", new ConfigOptionBools({false})); + + { + auto* grad_opt = project_config.option("filament_mixed_gradient"); + while (grad_opt->values.size() <= cfg_idx) + grad_opt->values.push_back(false); + grad_opt->values[cfg_idx] = result.gradient_enabled; + } + { + auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); + while (grad_range_opt->values.size() <= cfg_idx) + grad_range_opt->values.push_back(""); + if (result.gradient_enabled && result.components.size() == 2) { + const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; + grad_range_opt->values[cfg_idx] = fmt; + } else { + grad_range_opt->values[cfg_idx] = ""; + } + } + { + auto* grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); + while (grad_curve_opt->values.size() <= cfg_idx) + grad_curve_opt->values.push_back(""); + grad_curve_opt->values[cfg_idx] = serialize_mixed_gradient_curve_if_custom(result); + } + { + auto* per_part_opt = project_config.option("filament_mixed_gradient_per_part"); + while (per_part_opt->values.size() <= cfg_idx) + per_part_opt->values.push_back(false); + per_part_opt->values[cfg_idx] = result.gradient_enabled && result.per_part_gradient; + } + + // Compute blended color + std::string blended = blend_mixed_color(result.components, result.ratios, color_strs); + auto* colours_opt = project_config.option("filament_colour"); + if (colours_opt && cfg_idx < colours_opt->values.size()) + colours_opt->values[cfg_idx] = blended; + + auto* multi_colour_opt = project_config.option("filament_multi_colour"); + if (multi_colour_opt && cfg_idx < multi_colour_opt->values.size()) + multi_colour_opt->values[cfg_idx] = blended; + + // The edited slot keeps its index, so nothing else refreshes the per-feature filament + // lists - and its blended colour and type are what they show for it. + update_mixed_filament_list(); + update_dynamic_filament_list(); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(this, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, this)); + } +} + +void Sidebar::delete_mixed_filament_at(size_t panel_idx) +{ + auto* plater = dynamic_cast(GetParent()); + if (!plater) + return; + + auto mixed_indices = plater->mixed_filament_config_indices(); + if (panel_idx >= mixed_indices.size()) + return; + size_t cfg_idx = mixed_indices[panel_idx]; + + delete_filament(cfg_idx, -1); +} + +void Sidebar::decompose_filament_color(int filament_idx) +{ + if (filament_idx == kSidebarContextMenuFilamentId) + filament_idx = p->m_menu_filament_id; + if (filament_idx < 0) + return; + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* colours_opt = project_config.option("filament_colour"); + if (!colours_opt || static_cast(filament_idx) >= colours_opt->values.size()) + return; + + wxColour target_color(colours_opt->values[filament_idx]); + + std::vector color_strs, names, types; + std::vector physical_config_indices; + collect_physical_filament_info(color_strs, names, types, &physical_config_indices); + + // Build decompose-specific types: ColorDecomposeDialog needs "PLA Basic" + // distinction (for CMYW/RYBW card visibility), while collect_physical_filament_info + // now returns coarse filament_type (e.g. "PLA" for all PLA variants). + std::vector decompose_types; + { + auto& pb = *wxGetApp().preset_bundle; + for (size_t i = 0; i < physical_config_indices.size(); ++i) { + const size_t ci = physical_config_indices[i]; + Preset* pr = (ci < pb.filament_presets.size()) ? pb.filaments.find_preset(pb.filament_presets[ci]) : nullptr; + decompose_types.push_back(filament_type_for_color_decompose(pr)); + } + } + + size_t source_physical_idx = size_t(-1); + for (size_t i = 0; i < physical_config_indices.size(); ++i) { + if (physical_config_indices[i] == static_cast(filament_idx)) { + source_physical_idx = i; + break; + } + } + + ColorDecomposeDialog dlg(this, source_physical_idx == size_t(-1) ? -1 : static_cast(source_physical_idx), target_color, color_strs, + names, decompose_types, wxGetApp().preset_bundle->filament_presets.size(), + static_cast(EnforcerBlockerType::ExtruderMax), physical_config_indices); + int modal_res = dlg.ShowModal(); + if (modal_res == wxID_OK) { + ColorDecomposeResult dialog_result = dlg.get_result(); + MixedFilamentResult mixed_result; + std::vector missing_components; + if (!prepare_decompose_mixed_result(dialog_result, static_cast(filament_idx), source_physical_idx, color_strs, + decompose_types, physical_config_indices, mixed_result, missing_components)) + return; + + if (!confirm_create_decompose_missing_components(this, missing_components)) + return; + + for (const DecomposeMissingComponent& missing : missing_components) { + size_t before_count = p->combos_filament.size(); + add_custom_filament(wxColour(missing.official_component.color_hex), missing.preset_name, true); + size_t after_count = p->combos_filament.size(); + if (after_count <= before_count) + return; + set_created_standard_component_metadata(before_count, missing.official_component); + if (missing.component_idx < mixed_result.components.size()) + mixed_result.components[missing.component_idx] = static_cast(before_count + 1); + } + + if (!missing_components.empty()) { + collect_physical_filament_info(color_strs, names, types, &physical_config_indices); + } + + create_mixed_filament_from_result(this, mixed_result, color_strs); + } +} + void Sidebar::update_filaments_area_height() // ORCA { // ORCA use a height with user preference auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); - int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); + int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); auto height_with_borders = combo_sizer->GetSize().GetHeight(); // gets height from sizer instead static numbers p->m_panel_filament_content->SetMaxSize(wxSize{-1, preferred_rows * height_with_borders}); @@ -3710,18 +5114,18 @@ void Sidebar::update_filaments_area_height() void Sidebar::update_filaments_counter(bool force_layout) // ORCA { - int current_count = p->combos_filament.size(); - int preferred_count = std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count")); - bool isShown = p->m_panel_filament_content->IsShown(); - auto counter = p->m_staticText_filament_count; + int current_count = p->combos_filament.size(); + int preferred_count = std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count")); + bool isShown = p->m_panel_filament_content->IsShown(); + auto counter = p->m_staticText_filament_count; counter->SetLabel("(" + std::to_string(current_count) + ")"); // update counter on every change - if(current_count > preferred_count || !isShown) + if (current_count > preferred_count || !isShown) counter->Show(); else if (isShown) // hide when list is visible and short enough counter->Hide(); - if(force_layout) + if (force_layout) m_scrolled_sizer->Layout(); } @@ -3729,8 +5133,7 @@ void Sidebar::msw_rescale() { SetMinSize(wxSize(39 * wxGetApp().em_unit(), -1)); p->m_panel_printer_title->GetSizer()->SetMinSize(-1, 3 * wxGetApp().em_unit()); - p->m_panel_filament_title->GetSizer() - ->SetMinSize(-1, 3 * wxGetApp().em_unit()); + p->m_panel_filament_title->GetSizer()->SetMinSize(-1, 3 * wxGetApp().em_unit()); p->m_printer_icon->msw_rescale(); p->m_printer_connect->msw_rescale(); p->m_printer_bbl_sync->msw_rescale(); @@ -3752,14 +5155,15 @@ void Sidebar::msw_rescale() p->panel_printer_bed->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); p->panel_printer_bed->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->combo_printer_bed->Rescale(); - p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow - p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow - bool isDual = static_cast(p->panel_printer_preset->GetSizer())->GetOrientation() == wxVERTICAL; + p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow + p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow + bool isDual = static_cast(p->panel_printer_preset->GetSizer())->GetOrientation() == wxVERTICAL; auto image_path = get_cur_select_bed_image(); p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); - if (p->big_bed_image_popup){ // ORCA force rebuild frame. current wxwidget version not supports wxBITMAP_SCALE_FILL flag on wxStaticBitmap - // also wxImage scaledImage = bit_map.ConvertToImage(); scaledImage.Rescale(FromDIP(m_image_px), FromDIP(m_image_px), wxIMAGE_QUALITY_HIGH); - // didnt worked as expected and it requires use on set_bitmap. so that will try to scale everytime + if (p->big_bed_image_popup) { // ORCA force rebuild frame. current wxwidget version not supports wxBITMAP_SCALE_FILL flag on + // wxStaticBitmap also wxImage scaledImage = bit_map.ConvertToImage(); + // scaledImage.Rescale(FromDIP(m_image_px), FromDIP(m_image_px), wxIMAGE_QUALITY_HIGH); didnt worked as + // expected and it requires use on set_bitmap. so that will try to scale everytime p->big_bed_image_popup->Destroy(); p->big_bed_image_popup = nullptr; } @@ -3773,25 +5177,25 @@ void Sidebar::msw_rescale() p->m_flushing_volume_btn->Rescale(); set_flushing_volume_warning(is_flush_config_modified()); // ORCA reapply appearance - //BBS + // BBS p->left_extruder->Rescale(); p->right_extruder->Rescale(); p->single_extruder->Rescale(); - //p->btn_sync_printer->SetPaddingSize({FromDIP(6), FromDIP(12)}); - //p->btn_sync_printer->SetMinSize(BTN_SYNC_SIZE); - //p->btn_sync_printer->Rescale(); + // p->btn_sync_printer->SetPaddingSize({FromDIP(6), FromDIP(12)}); + // p->btn_sync_printer->SetMinSize(BTN_SYNC_SIZE); + // p->btn_sync_printer->Rescale(); #if 0 if (p->mode_sizer) p->mode_sizer->msw_rescale(); #endif - //for (PlaterPresetComboBox* combo : std::vector { p->combo_print, - // //p->combo_sla_print, - // //p->combo_sla_material, - // //p->combo_printer - // } ) - // combo->msw_rescale(); + // for (PlaterPresetComboBox* combo : std::vector { p->combo_print, + // //p->combo_sla_print, + // //p->combo_sla_material, + // //p->combo_printer + // } ) + // combo->msw_rescale(); for (PlaterPresetComboBox* combo : p->combos_filament) combo->msw_rescale(); @@ -3800,11 +5204,11 @@ void Sidebar::msw_rescale() update_filaments_area_height(); // ORCA resize after combos scaled // BBS - //p->frequently_changed_parameters->msw_rescale(); - //obj_list()->msw_rescale(); + // p->frequently_changed_parameters->msw_rescale(); + // obj_list()->msw_rescale(); // BBS TODO: add msw_rescale for newly added windows // BBS - //p->object_manipulation->msw_rescale(); + // p->object_manipulation->msw_rescale(); p->object_settings->msw_rescale(); p->m_search_item->Rescale(); p->m_search_item->GetTextCtrl()->SetSize(wxSize(-1, FromDIP(16))); @@ -3842,7 +5246,7 @@ void Sidebar::sys_color_changed() for (wxWindow* win : std::vector{ p->scrolled, p->presets_panel }) wxGetApp().UpdateAllStaticTextDarkUI(win); #endif - //p->btn_sync_printer->SetIcon("printer_sync"); + // p->btn_sync_printer->SetIcon("printer_sync"); p->m_printer_bbl_sync->msw_rescale(); p->m_printer_connect->msw_rescale(); // for (wxWindow* btn : std::vector{ p->btn_reslice, p->btn_export_gcode }) @@ -3867,7 +5271,7 @@ void Sidebar::sys_color_changed() #endif p->object_settings->sys_color_changed(); - //BBS: remove print related combos + // BBS: remove print related combos #if 0 for (PlaterPresetComboBox* combo : std::vector{ p->combo_print, p->combo_sla_print, @@ -3886,12 +5290,12 @@ void Sidebar::sys_color_changed() p->image_printer->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); p->image_printer_bed->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); - for (ExtruderGroup *ext : {p->left_extruder, p->right_extruder, p->single_extruder}) + for (ExtruderGroup* ext : {p->left_extruder, p->right_extruder, p->single_extruder}) if (ext) ext->sys_color_changed(); // call a kill focus event to ensure new colors applied - for (ComboBox* combo : std::vector{p->combo_printer, p->combo_nozzle_dia, p->combo_printer_bed}){ + for (ComboBox* combo : std::vector{p->combo_printer, p->combo_nozzle_dia, p->combo_printer_bed}) { wxFocusEvent fakeEvent(wxEVT_KILL_FOCUS); fakeEvent.SetEventObject(combo); combo->HandleWindowEvent(fakeEvent); @@ -3901,26 +5305,26 @@ void Sidebar::sys_color_changed() obj_list()->sys_color_changed(); obj_layers()->sys_color_changed(); // BBS - //p->object_manipulation->sys_color_changed(); + // p->object_manipulation->sys_color_changed(); // btn...->msw_rescale() updates icon on button, so use it - //p->btn_send_gcode->msw_rescale(); -// p->btn_eject_device->msw_rescale(); - //p->btn_export_gcode_removable->msw_rescale(); + // p->btn_send_gcode->msw_rescale(); + // p->btn_eject_device->msw_rescale(); + // p->btn_export_gcode_removable->msw_rescale(); p->scrolled->Layout(); + // Mixed rows are custom-drawn, so they need rebuilding for the new theme colours. + update_mixed_filament_list(); + p->searcher.dlg_sys_color_changed(); } -void Sidebar::search() -{ - p->searcher.search(); -} +void Sidebar::search() { p->searcher.search(); } void Sidebar::jump_to_option(const std::string& opt_key, Preset::Type type, const std::wstring& category) { - //const Search::Option& opt = p->searcher.get_option(opt_key, type); + // const Search::Option& opt = p->searcher.get_option(opt_key, type); if (type == Preset::TYPE_PRINT) { auto tab = dynamic_cast(wxGetApp().params_panel()->get_current_tab()); if (tab && tab->has_key(opt_key)) { @@ -3938,27 +5342,47 @@ void Sidebar::jump_to_option(size_t selected) jump_to_option(opt.opt_key(), opt.type, opt.category); // Switch to the Settings NotePad -// wxGetApp().mainframe->select_tab(); + // wxGetApp().mainframe->select_tab(); } // BBS. Move logic from Plater::on_extruders_change() to Sidebar::on_filament_count_change(). void Sidebar::on_filament_count_change(size_t num_filaments) { + // num_filaments counts every slot; mixed-color slots are virtual and get no combo of + // their own (they are rendered by update_mixed_filament_list instead), so the physical + // subset drives the combo list. + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + + std::vector physical_indices; + for (size_t i = 0; i < num_filaments; ++i) { + if (!is_mixed_opt || i >= is_mixed_opt->values.size() || !is_mixed_opt->values[i]) + physical_indices.push_back(i); + } + const size_t num_physical = physical_indices.size(); + auto& choices = combos_filament(); - if (num_filaments == choices.size()) + if (num_physical == choices.size()) { + // The ctor pre-creates one combo, so a single-filament project hits this guard before + // any layout pass has sized the scroll areas; refresh them here as well. + // Adding a mixed slot also lands here, since only the virtual count changed, so the + // per-feature filament lists - which do list mixed slots - have to be refreshed too. + recalc_filament_scroll_sizes(); + update_mixed_filament_list(); + update_dynamic_filament_list(); return; + } - if (choices.size() == 1 || num_filaments == 1) + if (choices.size() == 1 || num_physical == 1) choices[0]->GetDropDown().Invalidate(); wxWindowUpdateLocker noUpdates_scrolled_panel(this); size_t i = choices.size(); - while (i < num_filaments) - { - PlaterPresetComboBox* choice/*{ nullptr }*/; - init_filament_combo(&choice, i); + while (i < num_physical) { + PlaterPresetComboBox* choice /*{ nullptr }*/; + init_filament_combo(&choice, physical_indices[i]); int last_selection = choices.back()->GetSelection(); choices.push_back(choice); @@ -3969,11 +5393,13 @@ void Sidebar::on_filament_count_change(size_t num_filaments) } // remove unused choices if any - remove_unused_filament_combos(num_filaments); + remove_unused_filament_combos(num_physical); show_SEMM_buttons(); // ORCA - update_filaments_area_height(); // ORCA + update_filaments_area_height(); // ORCA + recalc_filament_scroll_sizes(); + update_mixed_filament_list(); Layout(); p->m_panel_filament_title->Refresh(); @@ -3983,71 +5409,80 @@ void Sidebar::on_filament_count_change(size_t num_filaments) void Sidebar::on_filaments_delete(size_t filament_id) { - auto &choices = combos_filament(); + auto& choices = combos_filament(); - if (filament_id >= choices.size()) - return; + // A mixed (virtual) slot has no combo of its own, so there is no combo UI to remove — + // but the shared refresh below must still run so the mixed filament panel drops its row. + if (filament_id < choices.size()) { + if (choices.size() == 1) + choices[0]->GetDropDown().Invalidate(); - if (choices.size() == 1) - choices[0]->GetDropDown().Invalidate(); + wxWindowUpdateLocker noUpdates_scrolled_panel(this); - wxWindowUpdateLocker noUpdates_scrolled_panel(this); + // delete UI item + { + const int last = p->combos_filament.size() - 1; + auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); + sizer_filaments->Remove(last / 2); - // delete UI item - if (filament_id < p->combos_filament.size()) { - const int last = p->combos_filament.size() - 1; - auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); - sizer_filaments->Remove(last / 2); + PlaterPresetComboBox* to_delete_combox = p->combos_filament[filament_id]; + (*p->combos_filament[last]).Destroy(); + p->combos_filament.pop_back(); - PlaterPresetComboBox* to_delete_combox = p->combos_filament[filament_id]; - (*p->combos_filament[last]).Destroy(); - p->combos_filament.pop_back(); + // BBS: filament double columns + auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); + if (p->combos_filament.size() < 2) { + sizer_filaments1->Clear(); + } else { + size_t c0 = sizer_filaments0->GetChildren().GetCount(); + size_t c1 = sizer_filaments1->GetChildren().GetCount(); + if (c0 < c1) + sizer_filaments1->Remove(c1 - 1); + else if (c0 > c1) + sizer_filaments1->AddStretchSpacer(1); + } + } - // BBS: filament double columns - auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); - auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); - if (p->combos_filament.size() < 2) { - sizer_filaments1->Clear(); - } else { - size_t c0 = sizer_filaments0->GetChildren().GetCount(); - size_t c1 = sizer_filaments1->GetChildren().GetCount(); - if (c0 < c1) - sizer_filaments1->Remove(c1 - 1); - else if (c0 > c1) - sizer_filaments1->AddStretchSpacer(1); + show_SEMM_buttons(); // ORCA + + for (size_t idx = filament_id; idx < p->combos_filament.size(); ++idx) { + p->combos_filament[idx]->update(); } } - show_SEMM_buttons(); // ORCA - - for (size_t idx = filament_id ; idx < p->combos_filament.size(); ++idx) { - p->combos_filament[idx]->update(); - } - update_filaments_area_height(); // ORCA + recalc_filament_scroll_sizes(); + update_mixed_filament_list(); Layout(); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - dynamic_filament_list.update(); + update_dynamic_filament_list(); } -void Sidebar::add_filament() { - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - wxColour new_col = Plater::get_next_color_for_filament(); +void Sidebar::add_filament() +{ + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; + wxColour new_col = Plater::get_next_color_for_filament(); add_custom_filament(new_col); auto filament_list = p->m_panel_filament_content; - if(!filament_list->IsShown()){ + if (!filament_list->IsShown()) { filament_list->Show(); // ORCA show list if its folded m_scrolled_sizer->Layout(); } filament_list->Scroll(-1, INT_MAX); // ORCA scroll to end of list on changes to inform user about filament count } -void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { - if (is_new_project_in_gcode3mf()) { return; } - if (p->combos_filament.size() <= 1) return; +void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) +{ + if (is_new_project_in_gcode3mf()) { + return; + } + if (p->combos_filament.size() <= 1) + return; size_t filament_count = p->combos_filament.size() - 1; if (filament_id == size_t(-2)) { @@ -4057,27 +5492,47 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { filament_id = filament_count; } - if (filament_id > filament_count) + // Mixed (virtual) slots have no combo of their own, so their config index lies past + // filament_count; bound explicit ids by the total slot count instead. + size_t total_filaments = wxGetApp().preset_bundle->filament_presets.size(); + if (filament_id > filament_count && filament_id >= total_filaments) return; - if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + bool is_mixed = (filament_id >= p->combos_filament.size()); + + if (!is_mixed) { + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + } + + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; + } } - if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { - p->editing_filament = -1; - } + // update_num_filaments() shrinks filament_is_mixed along with the other per-filament arrays, + // so snapshot it first — the paint cleanup below needs to know which slots were mixed + // *before* the delete to avoid discarding assignments to still-valid mixed slots. + std::vector is_mixed_snapshot; + if (auto* opt = wxGetApp().preset_bundle->project_config.option("filament_is_mixed")) + is_mixed_snapshot = opt->values; wxGetApp().preset_bundle->update_num_filaments(filament_id); - wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); - wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, replace_filament_id > (int)filament_id ? (replace_filament_id - 1) : replace_filament_id); + + // filament_count only counts physical combos, so with mixed slots present it is not the + // new number of slots; recompute from the shrunk preset list for the downstream updates. + size_t total_after_delete = wxGetApp().preset_bundle->filament_presets.size(); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); + wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, + replace_filament_id > (int) filament_id ? (replace_filament_id - 1) : replace_filament_id, + is_mixed_snapshot); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); wxGetApp().plater()->update(); auto filament_list = p->m_panel_filament_content; - if(!filament_list->IsShown()){ + if (!filament_list->IsShown()) { filament_list->Show(); // ORCA show list if its folded m_scrolled_sizer->Layout(); } @@ -4087,38 +5542,144 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { void Sidebar::change_filament(size_t from_id, size_t to_id) { + // Merging a physical filament into a mixed one that lists it as a component would delete + // the very filament the mix depends on, leaving it broken. Warn before doing so. + auto& pb = *wxGetApp().preset_bundle; + bool from_is_physical = !pb.is_mixed_filament(from_id); + bool to_is_mixed = pb.is_mixed_filament(to_id); + + if (from_is_physical && to_is_mixed) { + auto* comp_opt = pb.project_config.option("filament_mixed_components"); + if (comp_opt && to_id < comp_opt->values.size()) { + auto comps = Slic3r::parse_mixed_components(comp_opt->values[to_id]); + unsigned int from_1based = (unsigned int) from_id + 1; + bool target_uses_source = false; + for (unsigned int c : comps) { + if (c == from_1based) { + target_uses_source = true; + break; + } + } + if (target_uses_source) { + int ret = wxMessageBox(_L("The target mixed filament uses this physical filament as a component. " + "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), + _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + if (ret != wxOK) + return; + } + } + } + delete_filament(from_id, int(to_id)); } void Sidebar::edit_filament() { p->editing_filament = -1; - if (p->m_menu_filament_id >= 0 && p->m_menu_filament_id < p->combos_filament.size() - && p->combos_filament[p->m_menu_filament_id]->switch_to_tab()) + if (p->m_menu_filament_id >= 0 && p->m_menu_filament_id < p->combos_filament.size() && + p->combos_filament[p->m_menu_filament_id]->switch_to_tab()) p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -void Sidebar::add_custom_filament(wxColour new_col) { - if (is_new_project_in_gcode3mf()) { return; } - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; +void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_name, bool /*skip_preset_validation*/) +{ + if (is_new_project_in_gcode3mf()) { + return; + } + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; + if (wxGetApp().preset_bundle->filament_presets.size() >= MAXIMUM_EXTRUDER_NUMBER) + return; - int filament_count = p->combos_filament.size() + 1; - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + // Mixed-color slots are kept at the tail of the filament arrays, so a new physical + // filament has to be inserted just after the last physical one rather than appended. + // total == every slot (physical + mixed); insert_pos == the physical slot count. + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + size_t insert_pos = p->combos_filament.size(); + int filament_count = (int) (total + 1); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + + // Maintain physical-first ordering: rotate the new slot from end to insert_pos. + // No mixed slots -> insert_pos == total -> every rotate below is a no-op. + if (insert_pos < total) { + auto& presets = wxGetApp().preset_bundle->filament_presets; + std::rotate(presets.begin() + insert_pos, presets.begin() + total, presets.end()); + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto& ams_mc = wxGetApp().preset_bundle->ams_multi_color_filment; + + auto rotate_strings = [&](const char* key) { + if (auto* opt = project_config.option(key)) + if (opt->values.size() > total) + std::rotate(opt->values.begin() + insert_pos, opt->values.begin() + total, opt->values.end()); + }; + auto rotate_ints = [&](const char* key) { + if (auto* opt = project_config.option(key)) + if (opt->values.size() > total) + std::rotate(opt->values.begin() + insert_pos, opt->values.begin() + total, opt->values.end()); + }; + auto rotate_bools = [&](const char* key) { + if (auto* opt = project_config.option(key)) + if (opt->values.size() > total) + std::rotate(opt->values.begin() + insert_pos, opt->values.begin() + total, opt->values.end()); + }; + + rotate_strings("filament_colour"); + rotate_strings("filament_multi_colour"); + rotate_strings("filament_colour_type"); + rotate_ints("filament_map"); + rotate_ints("filament_nozzle_map"); + rotate_ints("filament_volume_map"); + rotate_bools("filament_is_mixed"); + rotate_strings("filament_mixed_components"); + rotate_strings("filament_mixed_sublayer_ratios"); + rotate_bools("filament_mixed_gradient"); + rotate_strings("filament_mixed_gradient_range"); + rotate_strings("filament_mixed_gradient_curve"); + rotate_bools("filament_mixed_gradient_per_part"); + + if (ams_mc.size() > total) + std::rotate(ams_mc.begin() + insert_pos, ams_mc.begin() + total, ams_mc.end()); + + // Remap object/volume extruder IDs and paint data: anything >= insert_pos+1 (1-based) shifts up by 1 + int threshold_1based = (int) (insert_pos + 1); + auto ebt_threshold = EnforcerBlockerType(threshold_1based); + for (auto* obj : wxGetApp().plater()->model().objects) { + if (obj->config.has("extruder")) { + int ext = obj->config.extruder(); + if (ext >= threshold_1based) + obj->config.set("extruder", ext + 1); + } + for (auto* vol : obj->volumes) { + if (vol->config.has("extruder")) { + int ext = vol->config.extruder(); + if (ext >= threshold_1based) + vol->config.set("extruder", ext + 1); + } + vol->mmu_segmentation_facets.shift_states_above(*vol, ebt_threshold, +1); + } + } + } + + if (!preset_name.empty() && wxGetApp().preset_bundle->filaments.find_preset(preset_name, false) && + insert_pos < wxGetApp().preset_bundle->filament_presets.size()) { + wxGetApp().preset_bundle->filament_presets[insert_pos] = preset_name; + } + wxGetApp().plater()->get_partplate_list().on_filament_added(filament_count); wxGetApp().plater()->on_filament_count_change(filament_count); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - auto_calc_flushing_volumes(filament_count - 1); + auto_calc_flushing_volumes(insert_pos); } bool Sidebar::is_new_project_in_gcode3mf() { if (p->plater->using_exported_file()) { - auto filename = p->plater->get_preview_only_filename(); - auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename); - MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"), - wxOK | wxICON_WARNING); + auto filename = p->plater->get_preview_only_filename(); + auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename); + MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"), wxOK | wxICON_WARNING); dlg.ShowModal(); p->plater->new_project(); return true; @@ -4179,7 +5740,8 @@ void Sidebar::on_bed_type_change(BedType bed_type) std::map Sidebar::build_filament_ams_list(MachineObject* obj) { std::map filament_ams_list; - if (!obj) return filament_ams_list; + if (!obj) + return filament_ams_list; // For pull-mode agents (e.g., HTTP REST API), refresh DevFilaSystem first auto* agent = wxGetApp().getDeviceManager()->get_agent(); @@ -4189,17 +5751,18 @@ std::map Sidebar::build_filament_ams_list(MachineObject } } - auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) { - BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") - % name % tray.setting_id % tray.m_fila_type % tray.color; + auto build_tray_config = [](DevAmsTray const& tray, std::string const& name, std::string ams_id, std::string slot_id) { + BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") % name % + tray.setting_id % tray.m_fila_type % tray.color; DynamicPrintConfig tray_config; tray_config.set_key_value("filament_id", new ConfigOptionStrings{tray.setting_id}); tray_config.set_key_value("tag_uid", new ConfigOptionStrings{tray.tag_uid}); tray_config.set_key_value("ams_id", new ConfigOptionStrings{ams_id}); tray_config.set_key_value("slot_id", new ConfigOptionStrings{slot_id}); tray_config.set_key_value("filament_type", new ConfigOptionStrings{tray.m_fila_type}); - tray_config.set_key_value("tray_name", new ConfigOptionStrings{ name }); - tray_config.set_key_value("filament_colour", new ConfigOptionStrings{into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX))}); + tray_config.set_key_value("tray_name", new ConfigOptionStrings{name}); + tray_config.set_key_value("filament_colour", + new ConfigOptionStrings{into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX))}); tray_config.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)}); tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists}); @@ -4208,22 +5771,23 @@ std::map Sidebar::build_filament_ams_list(MachineObject if (wxGetApp().preset_bundle) { info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id); } - tray_config.set_key_value("filament_is_support", new ConfigOptionBools{ info.has_value() ? info->is_support : false}); + tray_config.set_key_value("filament_is_support", new ConfigOptionBools{info.has_value() ? info->is_support : false}); for (int i = 0; i < tray.cols.size(); ++i) { - tray_config.opt("filament_multi_colour")->values.push_back(into_u8(wxColour("#" + tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); + tray_config.opt("filament_multi_colour") + ->values.push_back(into_u8(wxColour("#" + tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); } return tray_config; }; if (obj->ams_support_virtual_tray) { int extruder = 0x10000; // Main (first) extruder at right - for (auto & vt_tray : obj->vt_slot) { - filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255 + for (auto& vt_tray : obj->vt_slot) { + filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext", vt_tray.id, "0")); // 254 or 255 extruder = 0; } } - auto get_ams_name = [](int ams_id, int slot_id)->std::string { + auto get_ams_name = [](int ams_id, int slot_id) -> std::string { if (ams_id >= 0 && ams_id < 26) { char slot_name = slot_id + '1'; return std::string(1, 'A' + ams_id) + std::string(1, slot_name); @@ -4240,9 +5804,9 @@ std::map Sidebar::build_filament_ams_list(MachineObject int ams_id = std::stoi(ams.first); int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right for (auto tray : ams.second->GetTrays()) { - int slot_id = std::stoi(tray.first); - filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id), - build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id))); + int slot_id = std::stoi(tray.first); + filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id), build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), + std::to_string(ams_id), std::to_string(slot_id))); } } return filament_ams_list; @@ -4254,24 +5818,18 @@ bool Sidebar::sync_extruder_list() return p->sync_extruder_list(only_external_material); } -bool Sidebar::is_fila_switch_ready() -{ - return p->is_fila_switch_ready(); -} +bool Sidebar::is_fila_switch_ready() { return p->is_fila_switch_ready(); } -void Sidebar::reset_fila_switch() -{ - p->update_extruder_separator_icon(false, false); -} +void Sidebar::reset_fila_switch() { p->update_extruder_separator_icon(false, false); } -bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObject *obj) +bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObject* obj) { - if(!obj) + if (!obj) return false; - std::string machine_print_name = obj->get_show_printer_type(); - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); + std::string machine_print_name = obj->get_show_printer_type(); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); if (machine_print_name != target_model_id) { return false; } @@ -4282,17 +5840,12 @@ bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObj return true; } -void Sidebar::update_sync_status(const MachineObject *obj) -{ - p->update_sync_status(obj); -} +void Sidebar::update_sync_status(const MachineObject* obj) { p->update_sync_status(obj); } -int Sidebar::get_sidebar_pos_right_x() -{ - return this->GetScreenPosition().x + this->GetSize().x; -} +int Sidebar::get_sidebar_pos_right_x() { return this->GetScreenPosition().x + this->GetSize().x; } -void Sidebar::on_size(SimpleEvent &e) { +void Sidebar::on_size(SimpleEvent& e) +{ if (m_sna_dialog && m_sna_dialog->IsShown()) { pop_sync_nozzle_and_ams_dialog(); } @@ -4301,18 +5854,24 @@ void Sidebar::on_size(SimpleEvent &e) { } } -void Sidebar::on_full_screen(IntEvent &e) { - if (m_sna_dialog) { m_sna_dialog->on_full_screen(e); } - if (m_fna_dialog) { m_fna_dialog->on_full_screen(e); } +void Sidebar::on_full_screen(IntEvent& e) +{ + if (m_sna_dialog) { + m_sna_dialog->on_full_screen(e); + } + if (m_fna_dialog) { + m_fna_dialog->on_full_screen(e); + } } -void Sidebar::get_big_btn_sync_pos_size(wxPoint &pt, wxSize &size) +void Sidebar::get_big_btn_sync_pos_size(wxPoint& pt, wxSize& size) { size = p->m_printer_bbl_sync->GetSize(); - pt = p->m_printer_bbl_sync->GetScreenPosition(); + pt = p->m_printer_bbl_sync->GetScreenPosition(); } -void Sidebar::get_small_btn_sync_pos_size(wxPoint &pt, wxSize &size) { +void Sidebar::get_small_btn_sync_pos_size(wxPoint& pt, wxSize& size) +{ size = ams_btn->GetSize(); pt = ams_btn->GetScreenPosition(); } @@ -4328,23 +5887,22 @@ void Sidebar::load_ams_list(MachineObject* obj) filament_ams_list = build_filament_ams_list(obj); } - bool device_change = false; + bool device_change = false; const std::string& device = obj ? obj->get_dev_id() : ""; if (p->ams_list_device != device) { p->ams_list_device = device; device_change = true; } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); - if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) - { + if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) { return; } wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; - for (auto c : p->combos_filament){ + for (auto c : p->combos_filament) { c->update(); if (device_change) { - c->ShowBadge(false);//change printer,then clear badge + c->ShowBadge(false); // change printer,then clear badge } } @@ -4360,14 +5918,15 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) return; GUI::wxGetApp().sidebar().load_ams_list(obj); - auto & list = wxGetApp().preset_bundle->filament_ams_list; + auto& list = wxGetApp().preset_bundle->filament_ams_list; if (list.empty()) { auto printer_name = p->plater->get_selected_printer_name_in_combox(); - p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); + p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, + _L("Sync printer information")); return; } - bool exist_at_list_one_filament =false; - for (auto &cur : list) { + bool exist_at_list_one_filament = false; + for (auto& cur : list) { auto temp_config = cur.second; auto filament_type = temp_config.opt_string("filament_type", 0u); auto filament_color = temp_config.opt_string("filament_colour", 0u); @@ -4392,9 +5951,9 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) if (!ams_filament_ids.empty()) { boost::algorithm::split(list2, ams_filament_ids, boost::algorithm::is_any_of(",")); } - wxGetApp().plater()->update_all_plate_thumbnails(true);//preview thumbnail for sync_dlg + wxGetApp().plater()->update_all_plate_thumbnails(true); // preview thumbnail for sync_dlg SyncAmsInfoDialog::SyncInfo temp_info; - temp_info.use_dialog_pos = false; + temp_info.use_dialog_pos = false; temp_info.cancel_text_to_later = is_from_big_sync_btn; if (m_sync_dlg == nullptr) { m_sync_dlg = new SyncAmsInfoDialog(this, temp_info); @@ -4407,12 +5966,12 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) if (m_sync_dlg->is_dirty_filament()) { wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", false, true); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - dynamic_filament_list.update(); + update_dynamic_filament_list(); } m_sync_dlg->set_check_dirty_fialment(false); dlg_res = m_sync_dlg->ShowModal(); } else { - dlg_res =(int) wxID_YES; + dlg_res = (int) wxID_YES; } if (dlg_res == wxID_CANCEL) return; @@ -4424,7 +5983,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) list2.resize(list.size()); auto iter = list.begin(); for (int i = 0; i < list.size(); ++i, ++iter) { - auto & ams = iter->second; + auto& ams = iter->second; auto filament_id = ams.opt_string("filament_id", 0u); ams.set_key_value("filament_changed", new ConfigOptionBool{dlg_res == wxID_YES || list2[i] != filament_id}); list2[i] = filament_id; @@ -4432,47 +5991,49 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) // BBS:Record consumables information before synchronization std::vector color_before_sync; - std::vector is_support_before; + std::vector is_support_before; DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; - ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); for (int i = 0; i < p->combos_filament.size(); ++i) { is_support_before.push_back(is_support_filament(i)); color_before_sync.push_back(color_opt->values[i]); } MergeFilamentInfo merge_info; - std::vector> unknowns; - auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams"); + std::vector> unknowns; + auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams"); auto sync_color_only = wxGetApp().app_config->get("sync_ams_filament_mode") == "1"; - auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, sync_color_only); + auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, + sync_color_only); wxString detail; - for (auto & uk : unknowns) { + for (auto& uk : unknowns) { auto tray_name = uk.first->opt_string("tray_name", 0u); auto filament_type = uk.first->opt_string("filament_type", 0u); detail += from_u8("\n- " + tray_name + "(" + filament_type + ") ") + _L(uk.second); } if (n == 0) { - MessageDialog dlg(this, - _L("There are no compatible filaments, and sync is not performed.") + detail, - _L("Sync filaments with AMS"), wxOK); + MessageDialog dlg(this, _L("There are no compatible filaments, and sync is not performed.") + detail, _L("Sync filaments with AMS"), + wxOK); dlg.ShowModal(); return; } // Replace unknown filament IDs with the resolved preset's filament_id - auto &filaments = wxGetApp().preset_bundle->filaments; - auto &filament_presets = wxGetApp().preset_bundle->filament_presets; + auto& filaments = wxGetApp().preset_bundle->filaments; + auto& filament_presets = wxGetApp().preset_bundle->filament_presets; for (size_t i = 0; i < list2.size() && i < filament_presets.size(); ++i) { if (list2[i] == UNKNOWN_FILAMENT_ID) { - const Preset *resolved = filaments.find_preset(filament_presets[i]); + const Preset* resolved = filaments.find_preset(filament_presets[i]); if (resolved) list2[i] = resolved->filament_id; } } ams_filament_ids = boost::algorithm::join(list2, ","); - wxGetApp().app_config ->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); + wxGetApp().app_config->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); if (!unknowns.empty()) { MessageDialog dlg(this, - _L("There are some unknown or incompatible filaments mapped to generic preset.\nPlease update Orca Slicer or restart Orca Slicer to check if there is an update to system presets.") + detail, - _L("Sync filaments with AMS"), wxOK); + _L("There are some unknown or incompatible filaments mapped to generic preset.\nPlease update Orca Slicer or " + "restart Orca Slicer to check if there is an update to system presets.") + + detail, + _L("Sync filaments with AMS"), wxOK); dlg.ShowModal(); } if (!sync_color_only) { @@ -4490,11 +6051,9 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) for (int i = 0; i < p->combos_filament.size(); ++i) { if (i >= color_before_sync.size()) { auto_calc_flushing_volumes(i); - } - else if(color_before_sync[i] != color_opt->values[i] && wxGetApp().app_config->get("auto_calculate_flush") != "disabled"){ + } else if (color_before_sync[i] != color_opt->values[i] && wxGetApp().app_config->get("auto_calculate_flush") != "disabled") { auto_calc_flushing_volumes(i); - } - else if(is_support_filament(i) !=is_support_before[i] && wxGetApp().app_config->get("auto_calculate_flush") == "all"){ + } else if (is_support_filament(i) != is_support_before[i] && wxGetApp().app_config->get("auto_calculate_flush") == "all") { auto_calc_flushing_volumes(i); } } @@ -4508,15 +6067,16 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) update_dynamic_filament_list(); } else { wxGetApp().plater()->update_filament_colors_in_full_config(); - for (auto &c : p->combos_filament) + for (auto& c : p->combos_filament) c->update(); obj_list()->update_filament_colors(); update_dynamic_filament_list(); } - auto badge_combox_filament = [sync_color_only](PlaterPresetComboBox *c) { - auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") : - _L("Filament type and color information have been synchronized, but slot information is not included."); + auto badge_combox_filament = [sync_color_only](PlaterPresetComboBox* c) { + auto tip = sync_color_only ? + _L("Only filament color information has been synchronized from printer.") : + _L("Filament type and color information have been synchronized, but slot information is not included."); c->SetToolTip(tip); c->ShowBadge(true); }; @@ -4528,16 +6088,17 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) // empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The // previous per-tray index walked the full filament_ams_list (including the skipped empties), // so an empty slot before a loaded one dropped the badge for the trailing filaments. - for (auto &c : p->combos_filament) { + for (auto& c : p->combos_filament) { badge_combox_filament(c); } } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "prepare enable_merge_color_by_sync_ams"; - if (!merge_info.is_empty() && wxGetApp().app_config->get_bool("enable_merge_color_by_sync_ams")) { // merge same color and preset filament//use same ams - auto reduce_index = [](MergeFilamentInfo &merge_info,int value) { + if (!merge_info.is_empty() && + wxGetApp().app_config->get_bool("enable_merge_color_by_sync_ams")) { // merge same color and preset filament//use same ams + auto reduce_index = [](MergeFilamentInfo& merge_info, int value) { for (size_t i = 0; i < merge_info.merges.size(); i++) { - auto &cur = merge_info.merges[i]; + auto& cur = merge_info.merges[i]; for (size_t j = 0; j < cur.size(); j++) { if (value < cur[j]) { cur[j] = cur[j] - 1; @@ -4556,7 +6117,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) for (size_t i = 0; i < merge_info.merges.size(); i++) { auto& cur = merge_info.merges[i]; - for (int j = cur.size() -1; j >= 1 ; j--) { + for (int j = cur.size() - 1; j >= 1; j--) { auto last_index = cur[j]; change_filament(last_index, cur[0]); cur.erase(cur.begin() + j); @@ -4567,7 +6128,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) for (size_t i = 0; i < sync_ams_badges.size(); i++) { if (sync_ams_badges[i] == true) { if (i < p->combos_filament.size()) { - auto &c = p->combos_filament[i]; + auto& c = p->combos_filament[i]; badge_combox_filament(c); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: p->combos_filament array out of bound"; @@ -4581,7 +6142,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) } auto temp_index = iter.first; if (temp_index < p->combos_filament.size() && temp_index >= 0) { - auto &c = p->combos_filament[temp_index]; + auto& c = p->combos_filament[temp_index]; badge_combox_filament(c); } } @@ -4591,12 +6152,11 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog"; } - bool Sidebar::should_show_SEMM_buttons() { - PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); - auto cfg = preset_bundle.printers.get_edited_preset().config; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); + auto cfg = preset_bundle.printers.get_edited_preset().config; return cfg.opt_bool("single_extruder_multi_material") || is_bbl_vendor; } @@ -4606,23 +6166,22 @@ void Sidebar::show_SEMM_buttons() // ORCA if (!p || p->combos_filament.empty() || !p->m_bpButton_add_filament || !p->m_bpButton_del_filament || !p->m_flushing_volume_btn) return; - + bool is_multi_material = p->combos_filament.size() > 1; bool single_or_bbl = should_show_SEMM_buttons(); - bool is_single = single_or_bbl && !is_multi_material; // SINGLE EXTRUDER / BBL WITH 1 MATERIAL - bool is_multi = single_or_bbl && is_multi_material; // MULTI MATERIAL WITH SINGLE EXTRUDER - bool is_fixed = !is_single && !is_multi; // MULTI EXTRUDER / TOOLCHANGER / IDEX WITH FIXED MATERIAL + bool is_single = single_or_bbl && !is_multi_material; // SINGLE EXTRUDER / BBL WITH 1 MATERIAL + bool is_multi = single_or_bbl && is_multi_material; // MULTI MATERIAL WITH SINGLE EXTRUDER + bool is_fixed = !is_single && !is_multi; // MULTI EXTRUDER / TOOLCHANGER / IDEX WITH FIXED MATERIAL p->m_bpButton_add_filament->Show(single_or_bbl); p->m_bpButton_del_filament->Show(is_multi); - p->m_flushing_volume_btn->Show( is_multi); + p->m_flushing_volume_btn->Show(is_multi); if (is_multi) { - for (auto &c : p->combos_filament) + for (auto& c : p->combos_filament) c->edit_btn->SetBitmap_("menu_filament"); - } - else if (is_single || is_fixed) { - for (auto &c : p->combos_filament) + } else if (is_single || is_fixed) { + for (auto& c : p->combos_filament) c->edit_btn->SetBitmap_("edit"); } @@ -4663,44 +6222,27 @@ void Sidebar::enable_nozzle_count_edit(bool enable) void Sidebar::update_dynamic_filament_list() { dynamic_filament_list.update(); + dynamic_physical_filament_list.update(); } -PlaterPresetComboBox* Sidebar::printer_combox() -{ - return p->combo_printer; -} +PlaterPresetComboBox* Sidebar::printer_combox() { return p->combo_printer; } ObjectList* Sidebar::obj_list() { // BBS - //return obj_list(); + // return obj_list(); return p->m_object_list; } -ObjectSettings* Sidebar::obj_settings() -{ - return p->object_settings; -} +ObjectSettings* Sidebar::obj_settings() { return p->object_settings; } -ObjectLayers* Sidebar::obj_layers() -{ - return p->object_layers; -} +ObjectLayers* Sidebar::obj_layers() { return p->object_layers; } -wxPanel* Sidebar::scrolled_panel() -{ - return p->scrolled; -} +wxPanel* Sidebar::scrolled_panel() { return p->scrolled; } -wxPanel* Sidebar::print_panel() -{ - return p->m_panel_print_content; -} +wxPanel* Sidebar::print_panel() { return p->m_panel_print_content; } -wxPanel* Sidebar::filament_panel() -{ - return p->m_panel_filament_content; -} +wxPanel* Sidebar::filament_panel() { return p->m_panel_filament_content; } ConfigOptionsGroup* Sidebar::og_freq_chng_params(const bool is_fff) { @@ -4721,11 +6263,10 @@ wxButton* Sidebar::get_wiping_dialog_button() void Sidebar::set_flushing_volume_warning(const bool flushing_volume_modify) { - if(flushing_volume_modify){ + if (flushing_volume_modify) { p->m_flushing_volume_btn->SetStyle(ButtonStyle::Regular, ButtonType::Compact); p->m_flushing_volume_btn->SetBorderColor(wxColour("#FF6F00")); - } - else + } else p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); } @@ -4740,19 +6281,17 @@ void Sidebar::enable_buttons(bool enable) #endif } -bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } -bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } -bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } +bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } +bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } +bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } bool Sidebar::show_export_removable(bool show) const { return p->btn_export_gcode_removable->Show(show); } -//bool Sidebar::show_eject(bool show) const { return p->btn_eject_device->Show(show); } -//bool Sidebar::get_eject_shown() const { return p->btn_eject_device->IsShown(); } +// bool Sidebar::show_eject(bool show) const { return p->btn_eject_device->Show(show); } +// bool Sidebar::get_eject_shown() const { return p->btn_eject_device->IsShown(); } -bool Sidebar::is_multifilament() +bool Sidebar::is_multifilament() { return p->combos_filament.size() > 1; } + +void Sidebar::deal_btn_sync() { - return p->combos_filament.size() > 1; -} - -void Sidebar::deal_btn_sync() { m_begin_sync_printer_status = true; bool only_external_material; // Manual "sync machine" button: is_manual=true so an H2C pops the MultiNozzleSyncDialog to pick a nozzle option. @@ -4766,10 +6305,10 @@ void Sidebar::deal_btn_sync() { template void setup_dialog_position(T& info) { - auto plater = wxGetApp().plater(); - auto& sidebar = plater->sidebar(); - auto docking = plater->get_sidebar_docking_state(); - bool on_right = true; + auto plater = wxGetApp().plater(); + auto& sidebar = plater->sidebar(); + auto docking = plater->get_sidebar_docking_state(); + bool on_right = true; if (docking == Sidebar::Left) { on_right = true; @@ -4794,23 +6333,27 @@ template void setup_dialog_position(T& info) } } -void Sidebar::pop_sync_nozzle_and_ams_dialog() { +void Sidebar::pop_sync_nozzle_and_ams_dialog() +{ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; wxTheApp->CallAfter([this]() { SyncNozzleAndAmsDialog::InputInfo temp_na_info; - wxPoint big_btn_pt; - wxSize big_btn_size; + wxPoint big_btn_pt; + wxSize big_btn_size; wxGetApp().plater()->sidebar().get_big_btn_sync_pos_size(big_btn_pt, big_btn_size); - temp_na_info.dialog_pos = big_btn_pt + wxPoint(big_btn_size.x, big_btn_size.y) + wxPoint(FromDIP(big_btn_size.x / 10.f - 5), FromDIP(big_btn_size.y / 10.f)); + temp_na_info.dialog_pos = big_btn_pt + wxPoint(big_btn_size.x, big_btn_size.y) + + wxPoint(FromDIP(big_btn_size.x / 10.f - 5), FromDIP(big_btn_size.y / 10.f)); temp_na_info.dialog_pos.y += FromDIP(2); setup_dialog_position(temp_na_info); wxPoint small_btn_pt; - wxSize small_btn_size; + wxSize small_btn_size; get_small_btn_sync_pos_size(small_btn_pt, small_btn_size); temp_na_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); - if (m_fna_dialog) { m_fna_dialog->on_hide(); } + if (m_fna_dialog) { + m_fna_dialog->on_hide(); + } if (m_sna_dialog) { m_sna_dialog->Destroy(); m_sna_dialog = nullptr; @@ -4824,14 +6367,16 @@ void Sidebar::pop_finsish_sync_ams_dialog() { wxTheApp->CallAfter([this]() { wxPoint small_btn_pt; - wxSize small_btn_size; + wxSize small_btn_size; get_small_btn_sync_pos_size(small_btn_pt, small_btn_size); FinishSyncAmsDialog::InputInfo temp_fsa_info; - temp_fsa_info.dialog_pos.y = small_btn_pt.y; + temp_fsa_info.dialog_pos.y = small_btn_pt.y; setup_dialog_position(temp_fsa_info); - temp_fsa_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); - if (m_sna_dialog) { m_sna_dialog->on_hide(); } + temp_fsa_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); + if (m_sna_dialog) { + m_sna_dialog->on_hide(); + } if (m_fna_dialog) { m_fna_dialog->Destroy(); m_fna_dialog = nullptr; @@ -4839,42 +6384,38 @@ void Sidebar::pop_finsish_sync_ams_dialog() m_fna_dialog = new FinishSyncAmsDialog(temp_fsa_info); m_fna_dialog->on_show(); }); - } static std::vector get_search_inputs(ConfigOptionMode mode) { - std::vector ret {}; + std::vector ret{}; auto& tabs_list = wxGetApp().tabs_list; auto print_tech = wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology(); for (auto tab : tabs_list) if (tab->supports_printer_technology(print_tech)) - ret.emplace_back(Search::InputInfo {tab->get_config(), tab->type(), mode}); + ret.emplace_back(Search::InputInfo{tab->get_config(), tab->type(), mode}); return ret; } -void Sidebar::update_searcher() -{ - p->searcher.init(get_search_inputs(m_mode)); -} +void Sidebar::update_searcher() { p->searcher.init(get_search_inputs(m_mode)); } void Sidebar::update_mode() { m_mode = wxGetApp().get_mode(); - //BBS: remove print related combos + // BBS: remove print related combos update_searcher(); wxWindowUpdateLocker noUpdates(this); // BBS - //obj_list()->get_sizer()->Show(m_mode > comSimple); + // obj_list()->get_sizer()->Show(m_mode > comSimple); obj_list()->unselect_objects(); obj_list()->update_selections(); -// obj_list()->update_object_menu(); + // obj_list()->update_object_menu(); Layout(); } @@ -4886,14 +6427,14 @@ void Sidebar::collapse(bool collapse) { p->plater->collapse_sidebar(collapse); } #ifdef _MSW_DARK_MODE void Sidebar::show_mode_sizer(bool show) { - //p->mode_sizer->Show(show); + // p->mode_sizer->Show(show); } #endif void Sidebar::update_ui_from_settings() { // BBS - //p->object_manipulation->update_ui_from_settings(); + // p->object_manipulation->update_ui_from_settings(); // update Cut gizmo, if it's open p->plater->canvas3D()->update_gizmos_on_off_state(); p->plater->set_current_canvas_as_dirty(); @@ -4918,47 +6459,38 @@ bool Sidebar::show_object_list(bool show) const void Sidebar::finish_param_edit() { p->editing_filament = -1; } -std::vector& Sidebar::combos_filament() -{ - return p->combos_filament; -} +std::vector& Sidebar::combos_filament() { return p->combos_filament; } void Sidebar::clear_combos_filament_badge() { - auto &combos_filament = p->combos_filament; - for (auto &c : combos_filament) { // clear flag + auto& combos_filament = p->combos_filament; + for (auto& c : combos_filament) { // clear flag c->ShowBadge(false); } } -void Sidebar::udpate_combos_filament_badge() { - auto &combos_filament = p->combos_filament; - for (auto &c : combos_filament) { +void Sidebar::udpate_combos_filament_badge() +{ + auto& combos_filament = p->combos_filament; + for (auto& c : combos_filament) { auto selection = c->GetSelection(); auto select_flag = c->GetFlag(selection); auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; c->ShowBadge(ok); } - } -Search::OptionsSearcher& Sidebar::get_searcher() -{ - return p->searcher; -} +Search::OptionsSearcher& Sidebar::get_searcher() { return p->searcher; } -std::string& Sidebar::get_search_line() -{ - return p->searcher.search_string(); -} +std::string& Sidebar::get_search_line() { return p->searcher.search_string(); } static std::map printer_thumbnails = {}; void Sidebar::update_printer_thumbnail() { - auto& preset_bundle = wxGetApp().preset_bundle; - Preset & selected_preset = preset_bundle->printers.get_edited_preset(); - std::string printer_type = selected_preset.get_current_printer_type(preset_bundle); + auto& preset_bundle = wxGetApp().preset_bundle; + Preset& selected_preset = preset_bundle->printers.get_edited_preset(); + std::string printer_type = selected_preset.get_current_printer_type(preset_bundle); if (printer_thumbnails.find(printer_type) != printer_thumbnails.end()) // Use known cache first p->image_printer->SetBitmap(create_scaled_bitmap(printer_thumbnails[printer_type], this, PRINTER_THUMBNAIL_SIZE.GetHeight())); else { @@ -4974,7 +6506,7 @@ void Sidebar::update_printer_thumbnail() */ // Orca: try to use the printer model cover as the thumbnail - const auto model_name = selected_preset.config.opt_string("printer_model"); + const auto model_name = selected_preset.config.opt_string("printer_model"); std::string cover_file = model_name + "_cover.png"; for (auto vendor_profile : preset_bundle->vendors) { for (auto vendor_model : vendor_profile.second.models) { @@ -4998,29 +6530,27 @@ void Sidebar::update_printer_thumbnail() } } -void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extruder_id) { - +void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extruder_id) +{ std::vector filament_indices; std::vector extruder_indices; auto& preset_bundle = wxGetApp().preset_bundle; - auto filament_ptr = preset_bundle->project_config.option("filament_colour"); - int filament_count = filament_ptr ? filament_ptr->size() : 0; - int extruder_count = preset_bundle->get_printer_extruder_count(); + auto filament_ptr = preset_bundle->project_config.option("filament_colour"); + int filament_count = filament_ptr ? filament_ptr->size() : 0; + int extruder_count = preset_bundle->get_printer_extruder_count(); if (filament_idx < 0) { filament_indices.resize(filament_count); std::iota(filament_indices.begin(), filament_indices.end(), 0); - } - else { + } else { filament_indices.emplace_back(filament_idx); } if (extruder_id < 0) { extruder_indices.resize(extruder_count); std::iota(extruder_indices.begin(), extruder_indices.end(), 0); - } - else { + } else { extruder_indices.emplace_back(extruder_id); } @@ -5038,24 +6568,28 @@ void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extru p->plater->update(); } - void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int extruder_id) { auto& preset_bundle = wxGetApp().preset_bundle; - auto& project_config = preset_bundle->project_config; - const auto& full_config = wxGetApp().preset_bundle->full_config(); - auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; - size_t extruder_nums = preset_bundle->get_printer_extruder_count(); - int nozzle_flush_dataset = full_config.option("nozzle_flush_dataset")->values[extruder_id]; - std::vector init_matrix = get_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, extruder_id, extruder_nums); + // A mixed-colour slot is virtual and is never flushed to or from: leave its row and column + // alone (the flushing dialog hides them and only compares physical slots). + if (modify_id >= 0 && preset_bundle->is_mixed_filament((size_t) modify_id)) + return; + auto& project_config = preset_bundle->project_config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; + size_t extruder_nums = preset_bundle->get_printer_extruder_count(); + int nozzle_flush_dataset = full_config.option("nozzle_flush_dataset")->values[extruder_id]; + std::vector init_matrix = get_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, + extruder_id, extruder_nums); const std::vector& min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); - const auto* flush_multi_opt = project_config.option("flush_multiplier"); - float flush_multiplier = flush_multi_opt ? (float)flush_multi_opt->get_at(extruder_id) : 1.f; - std::vector matrix = init_matrix; - int m_max_flush_volume = Slic3r::g_max_flush_volume; - unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); + const auto* flush_multi_opt = project_config.option("flush_multiplier"); + float flush_multiplier = flush_multi_opt ? (float) flush_multi_opt->get_at(extruder_id) : 1.f; + std::vector matrix = init_matrix; + int m_max_flush_volume = Slic3r::g_max_flush_volume; + unsigned int m_number_of_extruders = (int) (sqrt(init_matrix.size()) + 0.001); const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); std::vector> multi_colours; @@ -5080,23 +6614,25 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int if (modify_id >= 0 && modify_id < multi_colours.size()) { for (int i = 0; i < multi_colours.size(); ++i) { + if (preset_bundle->is_mixed_filament((size_t) i)) + continue; // from to modify int from_idx = i; if (from_idx != modify_id) { Slic3r::FlushVolCalculator calculator(min_flush_volumes[from_idx], m_max_flush_volume, nozzle_flush_dataset); - int flushing_volume = 0; + int flushing_volume = 0; bool is_from_support = is_support_filament(from_idx); - bool is_to_support = is_support_filament(modify_id); + bool is_to_support = is_support_filament(modify_id); if (is_to_support) { flushing_volume = Slic3r::g_flush_volume_to_support; - } - else { + } else { for (int j = 0; j < multi_colours[from_idx].size(); ++j) { const wxColour& from = multi_colours[from_idx][j]; for (int k = 0; k < multi_colours[modify_id].size(); ++k) { const wxColour& to = multi_colours[modify_id][k]; - int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), to.Red(), to.Green(), to.Blue()); - flushing_volume = std::max(flushing_volume, volume); + int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), + to.Red(), to.Green(), to.Blue()); + flushing_volume = std::max(flushing_volume, volume); } } if (is_from_support) @@ -5110,18 +6646,18 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int if (to_idx != modify_id) { Slic3r::FlushVolCalculator calculator(min_flush_volumes[modify_id], m_max_flush_volume, nozzle_flush_dataset); bool is_from_support = is_support_filament(modify_id); - bool is_to_support = is_support_filament(to_idx); - int flushing_volume = 0; + bool is_to_support = is_support_filament(to_idx); + int flushing_volume = 0; if (is_to_support) { flushing_volume = Slic3r::g_flush_volume_to_support; - } - else { + } else { for (int j = 0; j < multi_colours[modify_id].size(); ++j) { const wxColour& from = multi_colours[modify_id][j]; for (int k = 0; k < multi_colours[to_idx].size(); ++k) { const wxColour& to = multi_colours[to_idx][k]; - int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), to.Red(), to.Green(), to.Blue()); - flushing_volume = std::max(flushing_volume, volume); + int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), + to.Red(), to.Green(), to.Blue()); + flushing_volume = std::max(flushing_volume, volume); } } if (is_from_support) @@ -5132,27 +6668,21 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int } } } - set_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, matrix, extruder_id, extruder_nums); + set_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, matrix, extruder_id, + extruder_nums); } -void Sidebar::jump_to_object(ObjectDataViewModelNode* item) -{ - p->jump_to_object(item); -} +void Sidebar::jump_to_object(ObjectDataViewModelNode* item) { p->jump_to_object(item); } -void Sidebar::can_search() -{ - p->can_search(); -} +void Sidebar::can_search() { p->can_search(); } class PlaterDropTarget : public wxFileDropTarget { public: - PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) { - this->SetDefaultAction(wxDragCopy); - } + PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) + { this->SetDefaultAction(wxDragCopy); } - virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); + virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames); private: MainFrame& m_mainframe; @@ -5160,16 +6690,16 @@ private: }; namespace { -bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) +bool emboss_svg(Plater& plater, const wxString& svg_file, const Vec2d& mouse_drop_position) { std::string svg_file_str = into_u8(svg_file); - GLCanvas3D* canvas = plater.canvas3D(); + GLCanvas3D* canvas = plater.canvas3D(); if (canvas == nullptr) return false; auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg); if (base_svg == nullptr) return false; - GLGizmoSVG* svg = dynamic_cast(base_svg); + GLGizmoSVG* svg = dynamic_cast(base_svg); if (svg == nullptr) return false; @@ -5180,80 +6710,71 @@ bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_dro return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART); } -} +} // namespace // State to manage showing after export notifications and device ejecting -enum ExportingStatus{ - NOT_EXPORTING, - EXPORTING_TO_REMOVABLE, - EXPORTING_TO_LOCAL -}; - +enum ExportingStatus { NOT_EXPORTING, EXPORTING_TO_REMOVABLE, EXPORTING_TO_LOCAL }; // TODO: listen on dark ui change class FloatFrame : public wxAuiFloatingFrame { public: FloatFrame(wxWindow* parent, wxAuiManager* ownerMgr, const wxAuiPaneInfo& pane) : wxAuiFloatingFrame(parent, ownerMgr, pane) - { - wxGetApp().UpdateFrameDarkUI(this); - } + { wxGetApp().UpdateFrameDarkUI(this); } }; class AuiMgr : public wxAuiManager { public: - AuiMgr() : wxAuiManager(){} + AuiMgr() : wxAuiManager() {} virtual wxAuiFloatingFrame* CreateFloatingFrame(wxWindow* parent, const wxAuiPaneInfo& p) override - { - return new FloatFrame(parent, this, p); - } + { return new FloatFrame(parent, this, p); } }; // Plater / private struct Plater::priv { // PIMPL back pointer ("Q-Pointer") - Plater *q; - Sidebar * sidebar; - MainFrame *main_frame; + Plater* q; + Sidebar* sidebar; + MainFrame* main_frame; MenuFactory menus; SelectMachineDialog* m_select_machine_dlg = nullptr; - SendMultiMachinePage* m_send_multi_dlg = nullptr; + SendMultiMachinePage* m_send_multi_dlg = nullptr; SendToPrinterDialog* m_send_to_sdcard_dlg = nullptr; - PublishDialog *m_publish_dlg = nullptr; + PublishDialog* m_publish_dlg = nullptr; // Data - Slic3r::DynamicPrintConfig *config; // FIXME: leak? - Slic3r::Print fff_print; - Slic3r::SLAPrint sla_print; - Slic3r::Model model; - PrinterTechnology printer_technology = ptFFF; + Slic3r::DynamicPrintConfig* config; // FIXME: leak? + Slic3r::Print fff_print; + Slic3r::SLAPrint sla_print; + Slic3r::Model model; + PrinterTechnology printer_technology = ptFFF; Slic3r::GCodeProcessorResult gcode_result; // GUI elements AuiMgr m_aui_mgr; wxString m_default_window_layout; - wxPanel* current_panel{ nullptr }; + wxPanel* current_panel{nullptr}; std::vector panels; struct SidebarLayout { - bool is_enabled{false}; - bool is_collapsed{false}; - bool show{false}; + bool is_enabled{false}; + bool is_collapsed{false}; + bool show{false}; } sidebar_layout; Bed3D bed; Camera camera; - //BBS: partplate related structure + // BBS: partplate related structure PartPlateList partplate_list; - //BBS: add a flag to ignore cancel event + // BBS: add a flag to ignore cancel event bool m_ignore_event{false}; bool m_slice_all{false}; - bool m_is_slicing {false}; + bool m_is_slicing{false}; // Missing-plugin set signatures (sorted full refs joined by '\n'), one per notification. They // gate plugin-load re-validation and avoid needlessly recreating the notification when the set // is unchanged. Whether missing plugins block slicing is derived directly from PluginResolver @@ -5262,17 +6783,17 @@ struct Plater::priv std::string m_cloud_missing_shown_sig; std::string m_inactive_shown_sig; std::string m_broken_shown_sig; - bool auto_reslice_pending {false}; - bool auto_reslice_after_cancel {false}; - bool m_is_publishing {false}; + bool auto_reslice_pending{false}; + bool auto_reslice_after_cancel{false}; + bool m_is_publishing{false}; int m_is_RightClickInLeftUI{-1}; int m_cur_slice_plate; - //BBS: m_slice_all in .gcode.3mf file case, set true when slice all - bool m_slice_all_only_has_gcode{ false }; + // BBS: m_slice_all in .gcode.3mf file case, set true when slice all + bool m_slice_all_only_has_gcode{false}; bool m_need_update{false}; - //BBS: add popup object table logic - //ObjectTableDialog* m_popup_table{ nullptr }; + // BBS: add popup object table logic + // ObjectTableDialog* m_popup_table{ nullptr }; #if ENABLE_ENVIRONMENT_MAP GLTexture environment_texture; @@ -5280,17 +6801,17 @@ struct Plater::priv Mouse3DController mouse3d_controller; View3D* view3D; // BBS - //GLToolbar view_toolbar; + // GLToolbar view_toolbar; GLToolbar collapse_toolbar; - Preview *preview; - AssembleView* assemble_view { nullptr }; - bool first_enter_assemble{ true }; + Preview* preview; + AssembleView* assemble_view{nullptr}; + bool first_enter_assemble{true}; std::unique_ptr notification_manager; ProjectDirtyStateManager dirty_state; - BackgroundSlicingProcess background_process; - bool suppressed_backround_processing_update { false }; + BackgroundSlicingProcess background_process; + bool suppressed_backround_processing_update{false}; // TODO: A mechanism would be useful for blocking the plater interactions: // objects would be frozen for the user. In case of arrange, an animation @@ -5300,22 +6821,22 @@ struct Plater::priv // UIThreadWorker can be used as a replacement for BoostThreadWorker if // no additional worker threads are desired (useful for debugging or profiling) PlaterWorker m_worker; - SLAImportDialog * m_sla_import_dlg; + SLAImportDialog* m_sla_import_dlg; - int m_job_prepare_state; + int m_job_prepare_state; - bool delayed_scene_refresh; - std::string delayed_error_message; + bool delayed_scene_refresh; + std::string delayed_error_message; - wxTimer background_process_timer; - wxTimer auto_reslice_timer; + wxTimer background_process_timer; + wxTimer auto_reslice_timer; - std::string label_btn_export; - std::string label_btn_send; + std::string label_btn_export; + std::string label_btn_send; - bool show_render_statistic_dialog{ false }; - bool show_wireframe{ false }; - bool wireframe_enabled{ true }; + bool show_render_statistic_dialog{false}; + bool show_wireframe{false}; + bool wireframe_enabled{true}; static const std::regex pattern_bundle; static const std::regex pattern_3mf; @@ -5325,10 +6846,9 @@ struct Plater::priv bool m_is_dark = false; - priv(Plater *q, MainFrame *main_frame); + priv(Plater* q, MainFrame* main_frame); ~priv(); - bool need_update() const { return m_need_update; } void set_need_update(bool need_update) { m_need_update = need_update; } @@ -5341,7 +6861,8 @@ struct Plater::priv Slic3r::put_other_changes(); dirty_state.update_from_presets(); } - int save_project_if_dirty(const wxString& reason) { + int save_project_if_dirty(const wxString& reason) + { int res = wxID_NO; if (dirty_state.is_dirty()) { MainFrame* mainframe = wxGetApp().mainframe; @@ -5349,11 +6870,13 @@ struct Plater::priv wxString suggested_project_name; wxString project_name = suggested_project_name = get_project_filename(".3mf"); if (suggested_project_name.IsEmpty()) { - fs::path output_file = get_export_file_path(FT_3MF); + fs::path output_file = get_export_file_path(FT_3MF); suggested_project_name = output_file.empty() ? _L("Untitled") : from_u8(output_file.stem().string()); } - res = MessageDialog(mainframe, reason + "\n" + format_wxstr(_L("Do you want to save changes to \"%1%\"?"), suggested_project_name), - wxString(SLIC3R_APP_FULL_NAME), wxYES_NO | wxCANCEL).ShowModal(); + res = MessageDialog(mainframe, + reason + "\n" + format_wxstr(_L("Do you want to save changes to \"%1%\"?"), suggested_project_name), + wxString(SLIC3R_APP_FULL_NAME), wxYES_NO | wxCANCEL) + .ShowModal(); if (res == wxID_YES) if (!mainframe->save_project_as(project_name)) res = wxID_CANCEL; @@ -5361,7 +6884,11 @@ struct Plater::priv } return res; } - void reset_project_dirty_after_save() { m_undo_redo_stack_main.mark_current_as_saved(); dirty_state.reset_after_save(); } + void reset_project_dirty_after_save() + { + m_undo_redo_stack_main.mark_current_as_saved(); + dirty_state.reset_after_save(); + } void reset_project_dirty_initial_presets() { dirty_state.reset_initial_presets(); } #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW @@ -5375,7 +6902,7 @@ struct Plater::priv }; void update(unsigned int flags = 0); void select_view(const std::string& direction); - //BBS: add no_slice option + // BBS: add no_slice option void select_view_3D(const std::string& name, bool no_slice = true); void select_next_view_3D(); @@ -5387,7 +6914,7 @@ struct Plater::priv bool are_view3D_labels_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->are_labels_shown(); } void show_view3D_labels(bool show) { - if (current_panel == view3D) { + if (current_panel == view3D) { view3D->get_canvas3d()->show_labels(show); wxGetApp().app_config->set_bool("show_labels", show); } @@ -5396,7 +6923,7 @@ struct Plater::priv bool is_view3D_overhang_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_overhang_shown(); } void show_view3D_overhang(bool show) { - if (current_panel == view3D) { + if (current_panel == view3D) { view3D->get_canvas3d()->show_overhang(show); wxGetApp().app_config->set_bool("show_overhang", show); } @@ -5408,14 +6935,15 @@ struct Plater::priv void reset_window_layout(); Sidebar::DockingState get_sidebar_docking_state(); - bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); } + bool is_view3D_layers_editing_enabled() const + { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); } void set_current_canvas_as_dirty(); GLCanvas3D* get_current_canvas3D(bool exclude_preview = false); void unbind_canvas_event_handlers(); void reset_canvas_volumes(); - bool check_ams_status_impl(bool is_slice_all); // Check whether the printer and ams status are consistent, for grouping algorithm - bool get_machine_sync_status(); // check whether the printer is linked and the printer type is same as selected profile + bool check_ams_status_impl(bool is_slice_all); // Check whether the printer and ams status are consistent, for grouping algorithm + bool get_machine_sync_status(); // check whether the printer is linked and the printer type is same as selected profile // BBS bool init_collapse_toolbar(); @@ -5444,13 +6972,47 @@ struct Plater::priv void update_ui_from_settings(); // BBS std::shared_ptr statusbar(); - std::string get_config(const std::string &key) const; + std::string get_config(const std::string& key) const; BoundingBoxf bed_shape_bb() const; BoundingBox scaled_bed_shape_bb() const; // BBS: backup & restore - std::vector load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi = false, bool* published_out = nullptr); - std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true); + using LoadProgressCallback = std::function; + std::vector load_files(const std::vector& input_files, + LoadStrategy strategy, + bool ask_multi = false, + bool* published_out = nullptr); + std::vector load_model_objects(const ModelObjectPtrs& model_objects, + bool allow_negative_z = false, + bool split_object = false, + bool auto_drop = true); + + // Texture-to-color import: a mesh loaded with UVs + a texture map gets its faces clustered + // into printable colours, which are then matched against (or added to) the filament list. + struct TextureImportResult + { + Slic3r::PaintedMesh painted; + std::vector matches; + std::vector> new_filament_colors; + std::vector new_filament_preset_names; + std::vector new_mixed_filaments; + std::vector filament_entries; + size_t existing_filament_count = 0; + bool skipped = false; + bool fallback_to_geometry_only = false; + wxString fallback_warning; + }; + + bool run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, + TextureImportResult& result, + std::function cancel_callback = {}, + std::function progress_callback = {}); + void apply_textured_mesh_import_result(Slic3r::Model& loaded_model, + const std::vector& obj_idxs, + const TextureImportResult& result, + LoadProgressCallback progress_callback = {}, + bool update_scene = true); + void handle_textured_mesh_import(Slic3r::Model& model, const std::vector& obj_idxs, std::function cancel_callback = {}); fs::path get_export_file_path(GUI::FileType file_type); wxString get_export_file(GUI::FileType file_type, const wxString& title = {}); @@ -5475,7 +7037,7 @@ struct Plater::priv void deselect_all(); void exit_gizmo(); void remove(size_t obj_idx); - bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS + bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); // BBS void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -5487,7 +7049,11 @@ struct Plater::priv void scale_selection_to_fit_print_volume(); // Return the active Undo/Redo stack. It may be either the main stack or the Gimzo stack. - Slic3r::UndoRedo::Stack& undo_redo_stack() { assert(m_undo_redo_stack_active != nullptr); return *m_undo_redo_stack_active; } + Slic3r::UndoRedo::Stack& undo_redo_stack() + { + assert(m_undo_redo_stack_active != nullptr); + return *m_undo_redo_stack_active; + } Slic3r::UndoRedo::Stack& undo_redo_stack_main() { return m_undo_redo_stack_main; } void enter_gizmos_stack(); bool leave_gizmos_stack(); @@ -5495,7 +7061,7 @@ struct Plater::priv void take_snapshot(const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action); /*void take_snapshot(const wxString& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action) { this->take_snapshot(std::string(snapshot_name.ToUTF8().data()), snapshot_type); }*/ - int get_active_snapshot_index(); + int get_active_snapshot_index(); void undo(); void redo(); @@ -5504,22 +7070,25 @@ struct Plater::priv // BBS: backup bool up_to_date(bool saved, bool backup); - void suppress_snapshots() { m_prevent_snapshots++; } - void allow_snapshots() { m_prevent_snapshots--; } + void suppress_snapshots() { m_prevent_snapshots++; } + void allow_snapshots() { m_prevent_snapshots--; } // BBS: single snapshot - void single_snapshots_enter(SingleSnapshot *single) + void single_snapshots_enter(SingleSnapshot* single) { - if (m_single == nullptr) m_single = single; + if (m_single == nullptr) + m_single = single; } - void single_snapshots_leave(SingleSnapshot *single) + void single_snapshots_leave(SingleSnapshot* single) { - if (m_single == single) m_single = nullptr; + if (m_single == single) + m_single = nullptr; } - void process_validation_warning(StringObjectException const &warning) const; - void process_validation_warnings(const std::vector &warnings) const; + void process_validation_warning(StringObjectException const& warning) const; + void process_validation_warnings(const std::vector& warnings) const; - bool background_processing_enabled() const { + bool background_processing_enabled() const + { #ifdef SUPPORT_BACKGROUND_PROCESSING return this->get_config("background_processing") == "1"; #else @@ -5531,7 +7100,7 @@ struct Plater::priv void schedule_background_process(); void schedule_auto_reslice_if_needed(); void trigger_auto_reslice_now(); - int auto_slice_delay_seconds() const; + int auto_slice_delay_seconds() const; // Update background processing thread from the current config and Model. enum UpdateBackgroundProcessReturnState { // update_background_process() reports, that the Print / SLAPrint was updated in a way, @@ -5554,7 +7123,8 @@ struct Plater::priv bool restart_background_process(unsigned int state); // returns bit mask of UpdateBackgroundProcessReturnState unsigned int update_restart_background_process(bool force_scene_update, bool force_preview_update); - void show_delayed_error_message() { + void show_delayed_error_message() + { if (!this->delayed_error_message.empty()) { std::string msg = std::move(this->delayed_error_message); this->delayed_error_message.clear(); @@ -5570,7 +7140,7 @@ struct Plater::priv void replace_all_with_stl(); void reload_all_from_disk(); - //BBS: add no_slice option + // BBS: add no_slice option void set_current_panel(wxPanel* panel, bool no_slice = true); void on_combobox_select(wxCommandEvent&); @@ -5584,9 +7154,9 @@ struct Plater::priv void on_slicing_began(); void clear_warnings(); - void add_warning(const Slic3r::PrintStateBase::Warning &warning, size_t oid); + void add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid); // Update notification manager with the current state of warnings produced by the background process (slicing). - void actualize_slicing_warnings(const PrintBase &print); + void actualize_slicing_warnings(const PrintBase& print); void actualize_object_warnings(const PrintBase& print); // Displays dialog window with list of warnings. // Returns true if user clicks OK. @@ -5599,32 +7169,32 @@ struct Plater::priv void on_action_split_objects(SimpleEvent&); void on_action_split_volumes(SimpleEvent&); void on_action_layersediting(SimpleEvent&); - void on_create_filament(SimpleEvent &); - void on_modify_filament(SimpleEvent &); - void on_add_filament(SimpleEvent &); - void on_delete_filament(SimpleEvent &); - void on_add_custom_filament(ColorEvent &); + void on_create_filament(SimpleEvent&); + void on_modify_filament(SimpleEvent&); + void on_add_filament(SimpleEvent&); + void on_delete_filament(SimpleEvent&); + void on_add_custom_filament(ColorEvent&); void on_object_select(SimpleEvent&); - void show_right_click_menu(Vec2d mouse_position, wxMenu *menu); + void show_right_click_menu(Vec2d mouse_position, wxMenu* menu); void on_right_click(RBtnEvent&); - //BBS: add model repair - void on_repair_model(wxCommandEvent &event); - void on_filament_color_changed(wxCommandEvent &event); - void show_install_plugin_hint(wxCommandEvent &event); - void install_network_plugin(wxCommandEvent &event); - void show_preview_only_hint(wxCommandEvent &event); - //BBS: add part plate related logic + // BBS: add model repair + void on_repair_model(wxCommandEvent& event); + void on_filament_color_changed(wxCommandEvent& event); + void show_install_plugin_hint(wxCommandEvent& event); + void install_network_plugin(wxCommandEvent& event); + void show_preview_only_hint(wxCommandEvent& event); + // BBS: add part plate related logic void on_plate_right_click(RBtnPlateEvent&); void on_plate_selected(SimpleEvent&); void on_action_request_model_id(wxCommandEvent& evt); void on_action_download_project(wxCommandEvent& evt); void on_slice_button_status(bool enable); - //BBS: GUI refactor: GLToolbar + // BBS: GUI refactor: GLToolbar void on_action_open_project(SimpleEvent&); void on_action_slice_plate(SimpleEvent&); void on_action_slice_all(SimpleEvent&); - void on_action_publish(wxCommandEvent &evt); + void on_action_publish(wxCommandEvent& evt); void on_action_print_plate(SimpleEvent&); void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); void on_action_print_all(SimpleEvent&); @@ -5633,7 +7203,7 @@ struct Plater::priv void on_action_export_sliced_file(SimpleEvent&); void on_action_export_all_sliced_file(SimpleEvent&); void on_action_select_sliced_plate(wxCommandEvent& evt); - //BBS: change dark/light mode + // BBS: change dark/light mode void on_change_color_mode(SimpleEvent& evt); void on_apple_change_color_mode(wxSysColourChangedEvent& evt); void apply_color_mode(); @@ -5641,9 +7211,9 @@ struct Plater::priv void on_3dcanvas_mouse_dragging_started(SimpleEvent&); void on_3dcanvas_mouse_dragging_finished(SimpleEvent&); - //void show_action_buttons(const bool is_ready_to_slice) const; + // void show_action_buttons(const bool is_ready_to_slice) const; bool show_publish_dlg(bool show = true); - void update_publish_dialog_status(wxString &msg, int percent = -1); + void update_publish_dialog_status(wxString& msg, int percent = -1); void on_action_print_plate_from_sdcard(SimpleEvent&); void on_tab_selection_changing(wxBookCtrlEvent&); @@ -5652,16 +7222,16 @@ struct Plater::priv // triangulate the bed and store the triangles into m_bed.m_triangles, // fills the m_bed.m_grid_lines and sets m_bed.m_origin. // Sets m_bed.m_polygon to limit the object placement. - //BBS: add bed exclude area - void set_bed_shape(const Pointfs &shape, - const Pointfs &exclude_areas, - const Pointfs &wrapping_exclude_areas, - const double printable_height, + // BBS: add bed exclude area + void set_bed_shape(const Pointfs& shape, + const Pointfs& exclude_areas, + const Pointfs& wrapping_exclude_areas, + const double printable_height, std::vector extruder_areas, - std::vector extruder_heights, - const std::string &custom_texture, - const std::string &custom_model, - bool force_as_custom = false); + std::vector extruder_heights, + const std::string& custom_texture, + const std::string& custom_model, + bool force_as_custom = false); bool can_delete() const; bool can_delete_all() const; @@ -5679,7 +7249,7 @@ struct Plater::priv bool can_set_instance_to_object() const; bool can_mirror() const; bool can_reload_from_disk() const; - //BBS: + // BBS: bool can_fillcolor() const; bool has_assemble_view() const; bool can_replace_with_stl() const; @@ -5689,12 +7259,15 @@ struct Plater::priv bool can_scale_to_print_volume() const; #endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT - //BBS: add plate_id for thumbnail - void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, - Camera::EType camera_type, - Camera::ViewAngleType camera_view_angle_type = Camera::ViewAngleType::Iso, - bool for_picking = false, - bool ban_light = false); + // BBS: add plate_id for thumbnail + void generate_thumbnail(ThumbnailData& data, + unsigned int w, + unsigned int h, + const ThumbnailsParams& thumbnail_params, + Camera::EType camera_type, + Camera::ViewAngleType camera_view_angle_type = Camera::ViewAngleType::Iso, + bool for_picking = false, + bool ban_light = false); ThumbnailsList generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type); PlateBBoxData generate_first_layer_bbox(); @@ -5706,7 +7279,7 @@ struct Plater::priv wxString get_export_gcode_filename(const wxString& extension = wxEmptyString, bool only_filename = false, bool export_all = false) const; void set_project_filename(const wxString& filename); - //BBS store bbs project name + // BBS store bbs project name wxString get_project_name(); void set_project_name(const wxString& project_name); void update_title_dirty_status(); @@ -5716,27 +7289,34 @@ struct Plater::priv void update_objects_position_when_select_preset(const std::function& select_prest); - // Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will not override it. - //mutable bool ready_to_slice = { false }; - // Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case when the background processing finishes. - ExportingStatus exporting_status { NOT_EXPORTING }; - std::string last_output_path; - std::string last_output_dir_path; - //BBS store machine_sn and 3mf_path for PrintJob - PrintPrepareData m_print_job_data; - bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } - int process_completed_with_error { -1 }; //-1 means no error + // Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will + // not override it. + // mutable bool ready_to_slice = { false }; + // Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case + // when the background processing finishes. + ExportingStatus exporting_status{NOT_EXPORTING}; + std::string last_output_path; + std::string last_output_dir_path; + // BBS store machine_sn and 3mf_path for PrintJob + PrintPrepareData m_print_job_data; + bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } + int process_completed_with_error{-1}; //-1 means no error - //BBS: project - BBLProject project; + // BBS: project + BBLProject project; - //BBS: add print project related logic + // BBS: add print project related logic void update_fff_scene_only_shells(bool only_shells = true); - //BBS: add popup object table logic + // BBS: add popup object table logic bool PopupObjectTable(int object_id, int volume_id, const wxPoint& position); void on_action_send_to_printer(bool isall = false); void on_action_send_to_multi_machine(SimpleEvent&); - int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path); + int update_print_required_data(Slic3r::DynamicPrintConfig config, + Slic3r::Model model, + Slic3r::PlateDataPtrs plate_data_list, + std::string file_name, + std::string file_path); + private: bool layers_height_allowed() const; @@ -5749,30 +7329,30 @@ private: void on_action_export_to_sdcard_all(SimpleEvent&); void update_plugin_when_launch(wxCommandEvent& event); // path to project folder stored with no extension - boost::filesystem::path m_project_folder; + boost::filesystem::path m_project_folder; /* display project name */ - wxString m_project_name; + wxString m_project_name; - Slic3r::UndoRedo::Stack m_undo_redo_stack_main; - Slic3r::UndoRedo::Stack m_undo_redo_stack_gizmos; - Slic3r::UndoRedo::Stack *m_undo_redo_stack_active = &m_undo_redo_stack_main; - int m_prevent_snapshots = 0; /* Used for avoid of excess "snapshoting". - * Like for "delete selected" or "set numbers of copies" - * we should call tack_snapshot just ones - * instead of calls for each action separately - * */ + Slic3r::UndoRedo::Stack m_undo_redo_stack_main; + Slic3r::UndoRedo::Stack m_undo_redo_stack_gizmos; + Slic3r::UndoRedo::Stack* m_undo_redo_stack_active = &m_undo_redo_stack_main; + int m_prevent_snapshots = 0; /* Used for avoid of excess "snapshoting". + * Like for "delete selected" or "set numbers of copies" + * we should call tack_snapshot just ones + * instead of calls for each action separately + * */ // BBS: single snapshot - Plater::SingleSnapshot *m_single = nullptr; + Plater::SingleSnapshot* m_single = nullptr; // BBS: backup - size_t m_saved_timestamp = 0; + size_t m_saved_timestamp = 0; size_t m_backup_timestamp = 0; - std::string m_last_fff_printer_profile_name; - std::string m_last_sla_printer_profile_name; + std::string m_last_fff_printer_profile_name; + std::string m_last_sla_printer_profile_name; // vector of all warnings generated by last slicing std::vector> current_warnings; - bool show_warning_dialog { false }; + bool show_warning_dialog{false}; }; Plater::~Plater() = default; @@ -5783,7 +7363,7 @@ const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::ica const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase); const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase); -bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) +bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames) { #ifdef WIN32 // hides the system icon @@ -5797,14 +7377,14 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi // When only one .svg file is dropped on scene if (filenames.size() == 1) { - const wxString &filename = filenames.Last(); - const wxString file_extension = filename.substr(filename.length() - 4); + const wxString& filename = filenames.Last(); + const wxString file_extension = filename.substr(filename.length() - 4); if (file_extension.CmpNoCase(".svg") == 0) { // BBS: GUI refactor: move sidebar to the left - const wxPoint offset = m_plater.GetPosition() + m_plater.p->current_panel->GetPosition(); + const wxPoint offset = m_plater.GetPosition() + m_plater.p->current_panel->GetPosition(); Vec2d mouse_position(x - offset.x, y - offset.y); // Scale for retina displays - const GLCanvas3D *canvas = m_plater.canvas3D(); + const GLCanvas3D* canvas = m_plater.canvas3D(); canvas->apply_retina_scale(mouse_position); return emboss_svg(m_plater, filename, mouse_position); } @@ -5814,29 +7394,26 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi return res; } -Plater::priv::priv(Plater *q, MainFrame *main_frame) +Plater::priv::priv(Plater* q, MainFrame* main_frame) : q(q) , main_frame(main_frame) - //BBS: add bed_exclude_area - , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({ - "printable_area", "bed_exclude_area", "wrapping_exclude_area", "extruder_printable_area", "bed_custom_texture", "bed_custom_model", "print_sequence", - "extruder_clearance_radius", - "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", - "nozzle_height", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", - "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", - "enable_prime_tower", "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "prime_tower_enable_framework", - "prime_tower_infill_gap", "prime_volume", - "extruder_colour", "filament_colour", "filament_type", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", - // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. - "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "wall_loops", "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", "top_shell_layers", - "enable_support", "support_filament", "support_interface_filament", - "support_top_z_distance", "support_bottom_z_distance", "raft_layers", - "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", - "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", - "wipe_tower_filament", - "best_object_pos", "master_extruder_id" - })) + // BBS: add bed_exclude_area + , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys( + {"printable_area", "bed_exclude_area", "wrapping_exclude_area", "extruder_printable_area", "bed_custom_texture", + "bed_custom_model", "print_sequence", "extruder_clearance_radius", "extruder_clearance_height_to_lid", + "extruder_clearance_height_to_rod", "nozzle_height", "skirt_type", "skirt_loops", "skirt_speed", "min_skirt_length", + "skirt_distance", "skirt_start_angle", "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", + "combine_brims", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", "enable_prime_tower", + "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", + "prime_tower_enable_framework", "prime_tower_infill_gap", "prime_volume", "extruder_colour", "filament_colour", "filament_type", + "filament_is_support", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", + // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. + "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", "wall_loops", "outer_wall_filament_id", + "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", "top_shell_layers", "enable_support", + "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", + "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length", "wipe_tower_rib_width", + "wipe_tower_fillet_wall", "wipe_tower_filament", "best_object_pos", "master_extruder_id"})) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} @@ -5844,7 +7421,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) , m_job_prepare_state(Job::JobPrepareState::PREPARE_STATE_DEFAULT) , delayed_scene_refresh(false) , collapse_toolbar(GLToolbar::Normal, "Collapse") - //BBS :partplatelist construction + // BBS :partplatelist construction , partplate_list(this->q, &model) { m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; @@ -5859,21 +7436,21 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) if (disable_wayland_floating) m_aui_mgr.SetFlags(m_aui_mgr.GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING); #endif - //m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_PANE_BORDER_SIZE, 0); - //m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_SASH_SIZE, 2); + // m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_PANE_BORDER_SIZE, 0); + // m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_SASH_SIZE, 2); m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, 18); m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_GRADIENT_TYPE, wxAUI_GRADIENT_NONE); this->q->SetFont(Slic3r::GUI::wxGetApp().normal_font()); - //BBS: use the first partplate's print for background process + // BBS: use the first partplate's print for background process partplate_list.update_slice_context_to_current_plate(background_process); /* background_process.set_fff_print(&fff_print); background_process.set_sla_print(&sla_print); background_process.set_gcode_result(&gcode_result); - background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); - background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); + background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, + Camera::EType::Ortho); }); background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); background_process.set_export_began_event(EVT_EXPORT_BEGAN); // Default printer technology for default config. @@ -5887,7 +7464,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) sla_print.set_status_callback(statuscb); */ // BBS: to be checked. Not follow patch. - background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); + background_process.set_thumbnail_cb( + [this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); background_process.set_export_began_event(EVT_EXPORT_BEGAN); @@ -5910,16 +7488,17 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); auto* panel_3d = new wxPanel(q); - view3D = new View3D(panel_3d, bed, &model, config, &background_process); - //BBS: use partplater's gcode - preview = new Preview(panel_3d, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), [this]() { schedule_background_process(); }); + view3D = new View3D(panel_3d, bed, &model, config, &background_process); + // BBS: use partplater's gcode + preview = new Preview(panel_3d, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), + [this]() { schedule_background_process(); }); assemble_view = new AssembleView(panel_3d, bed, &model, config, &background_process); #ifdef __APPLE__ // BBS // set default view_toolbar icons size equal to GLGizmosManager::Default_Icons_Size - //view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size); + // view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size); #endif // __APPLE__ panels.push_back(view3D); @@ -5928,8 +7507,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->background_process_timer.SetOwner(this->q, 0); this->auto_reslice_timer.SetOwner(this->q, 0); - this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent &evt) - { + this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent& evt) { if (&evt.GetTimer() == &this->background_process_timer) { if (!this->suppressed_backround_processing_update) this->update_restart_background_process(false, false); @@ -5944,13 +7522,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) update(); // Orca: Make sidebar dockable - m_aui_mgr.AddPane(sidebar, wxAuiPaneInfo() - .Name("sidebar") - .Left() - .CloseButton(false) - .TopDockable(false) - .BottomDockable(false) - .BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); + m_aui_mgr.AddPane(sidebar, wxAuiPaneInfo().Name("sidebar").Left().CloseButton(false).TopDockable(false).BottomDockable(false).BestSize( + wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL); panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0); @@ -5965,8 +7538,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) auto& sidebar = m_aui_mgr.GetPane(this->sidebar); // Load previous window layout - const auto cfg = wxGetApp().app_config; - wxString layout = wxString::FromUTF8(cfg->get("window_layout")); + const auto cfg = wxGetApp().app_config; + wxString layout = wxString::FromUTF8(cfg->get("window_layout")); if (!layout.empty()) { bool removed_floating_state = false; #ifdef __WXGTK__ @@ -6000,7 +7573,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) menus.init(main_frame); - // Events: if (wxGetApp().is_editor()) { @@ -6012,15 +7584,14 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // jump to found option from SearchDialog q->Bind(wxCUSTOMEVT_JUMP_TO_OPTION, [this](wxCommandEvent& evt) { sidebar->jump_to_option(evt.GetInt()); }); q->Bind(wxCUSTOMEVT_JUMP_TO_OBJECT, [this](wxCommandEvent& evt) { - auto client_data = evt.GetClientData(); + auto client_data = evt.GetClientData(); ObjectDataViewModelNode* data = static_cast(client_data); sidebar->jump_to_object(data); - } - ); + }); } wxGLCanvas* view3D_canvas = view3D->get_wxglcanvas(); - //BBS: GUI refactor + // BBS: GUI refactor wxGLCanvas* preview_canvas = preview->get_wxglcanvas(); if (wxGetApp().is_editor()) { @@ -6028,30 +7599,34 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { delayed_error_message.clear(); this->background_process_timer.Start(500, wxTIMER_ONE_SHOT); - }); + }); view3D_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this); view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); - //BBS: add part plate related logic + // BBS: add part plate related logic view3D_canvas->Bind(EVT_GLCANVAS_PLATE_RIGHT_CLICK, &priv::on_plate_right_click, this); view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); }); view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent& evt) { - //BBS arrange from EVT set default state. + // BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); - this->q->arrange(); }); + this->q->arrange(); + }); view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_PARTPLATE, [this](SimpleEvent& evt) { - //BBS arrange from EVT set default state. + // BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_MENU); - this->q->arrange(); }); + this->q->arrange(); + }); view3D_canvas->Bind(EVT_GLCANVAS_ORIENT, [this](SimpleEvent& evt) { - //BBS orient from EVT set default state. + // BBS orient from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); - this->q->orient(); }); + this->q->orient(); + }); view3D_canvas->Bind(EVT_GLCANVAS_ORIENT_PARTPLATE, [this](SimpleEvent& evt) { - //BBS orient from EVT set default state. + // BBS orient from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_MENU); - this->q->orient(); }); - //BBS - view3D_canvas->Bind(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, [this](SimpleEvent&) {this->q->select_curr_plate_all(); }); + this->q->orient(); + }); + // BBS + view3D_canvas->Bind(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, [this](SimpleEvent&) { this->q->select_curr_plate_all(); }); view3D_canvas->Bind(EVT_GLCANVAS_PRINTABLE, [this](SimpleEvent& evt) { this->sidebar->obj_list()->toggle_printable_state(); }); view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); }); @@ -6060,14 +7635,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) if (this->q->is_view3D_shown()) wxGetApp().open_speed_dial(); }); - view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event& evt) - { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); + view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event& evt) { + if (evt.data == 1) + this->q->increase_instances(); + else if (this->can_decrease_instances()) + this->q->decrease_instances(); + }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); // BBS - //view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); + // view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { on_slice_button_status(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_GEOMETRY, &priv::on_update_geometry, this); view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, &priv::on_3dcanvas_mouse_dragging_started, this); @@ -6076,83 +7655,89 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_RESETGIZMOS, [this](SimpleEvent&) { reset_all_gizmos(); }); view3D_canvas->Bind(EVT_GLCANVAS_UNDO, [this](SimpleEvent&) { this->undo(); }); view3D_canvas->Bind(EVT_GLCANVAS_REDO, [this](SimpleEvent&) { this->redo(); }); - view3D_canvas->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); - view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); }); - view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, [this](Event& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); }); - view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, + [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); + view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, + [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, + [this](Event& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, + [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->reload_all_from_disk(); }); // 3DScene/Toolbar: view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this); view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [q](SimpleEvent&) { q->remove_selected(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [this](SimpleEvent&) { delete_all_objects_from_model(); }); -// view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); }); + // view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_ADD_PLATE, &priv::on_action_add_plate, this); view3D_canvas->Bind(EVT_GLTOOLBAR_DEL_PLATE, &priv::on_action_del_plate, this); view3D_canvas->Bind(EVT_GLTOOLBAR_ORIENT, [this](SimpleEvent&) { - //BBS arrange from EVT set default state. + // BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); - this->q->orient(); }); + this->q->orient(); + }); view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { - //BBS arrange from EVT set default state. + // BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); this->q->arrange(); - }); + }); view3D_canvas->Bind(EVT_GLTOOLBAR_CUT, [q](SimpleEvent&) { q->cut_selection_to_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_COPY, [q](SimpleEvent&) { q->copy_selection_to_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [q](SimpleEvent&) { q->paste_from_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this); - //BBS: add clone + // BBS: add clone view3D_canvas->Bind(EVT_GLTOOLBAR_CLONE, [q](SimpleEvent&) { q->clone_selection(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [q](SimpleEvent&) { q->increase_instances(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_FEWER, [q](SimpleEvent&) { q->decrease_instances(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this); - //BBS: GUI refactor: GLToolbar + // BBS: GUI refactor: GLToolbar view3D_canvas->Bind(EVT_GLTOOLBAR_OPEN_PROJECT, &priv::on_action_open_project, this); - //view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); - //view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); - //view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); - //view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); - //view3D_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + // view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + // view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); + // view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + // view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + // view3D_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); view3D_canvas->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); - //preview also send these events - //preview_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); - //preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); - //preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); - //review_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + // preview also send these events + // preview_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + // preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + // preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + // review_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); view3D_canvas->Bind(EVT_GLCANVAS_SWITCH_TO_OBJECT, [main_frame](SimpleEvent&) { - if (main_frame->m_param_panel) { - main_frame->m_param_panel->switch_to_object(false); - } - }); + if (main_frame->m_param_panel) { + main_frame->m_param_panel->switch_to_object(false); + } + }); view3D_canvas->Bind(EVT_GLCANVAS_SWITCH_TO_GLOBAL, [main_frame](SimpleEvent&) { - if (main_frame->m_param_panel) { - main_frame->m_param_panel->switch_to_global(); - } - }); + if (main_frame->m_param_panel) { + main_frame->m_param_panel->switch_to_global(); + } + }); } view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); // Preview events: preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); - preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE, [this](SimpleEvent &) { - preview->get_canvas3d()->set_as_dirty(); - }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE, [this](SimpleEvent&) { preview->get_canvas3d()->set_as_dirty(); }); if (wxGetApp().is_editor()) { preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); }); - preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, + [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); preview->get_wxglcanvas()->Bind(EVT_CUSTOMEVT_TICKSCHANGED, [this](wxCommandEvent& event) { - Type tick_event_type = (Type)event.GetInt(); - Model& model = wxGetApp().plater()->model(); - //BBS: replace model custom gcode with current plate custom gcode - model.plates_custom_gcodes[model.curr_plate_index] = preview->get_canvas3d()->get_gcode_viewer().get_layers_slider()->GetTicksValues(); + Type tick_event_type = (Type) event.GetInt(); + Model& model = wxGetApp().plater()->model(); + // BBS: replace model custom gcode with current plate custom gcode + model.plates_custom_gcodes[model.curr_plate_index] = + preview->get_canvas3d()->get_gcode_viewer().get_layers_slider()->GetTicksValues(); // BBS set to invalid state only - if (tick_event_type == Type::ToolChange || tick_event_type == Type::Custom || tick_event_type == Type::Template || tick_event_type == Type::PausePrint) { - PartPlate *plate = this->q->get_partplate_list().get_curr_plate(); + if (tick_event_type == Type::ToolChange || tick_event_type == Type::Custom || tick_event_type == Type::Template || + tick_event_type == Type::PausePrint) { + PartPlate* plate = this->q->get_partplate_list().get_curr_plate(); if (plate) { plate->update_slice_result_valid_state(false); } @@ -6169,7 +7754,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) if (wxGetApp().is_gcode_viewer()) preview->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->q->reload_gcode_from_disk(); }); - //BBS + // BBS wxGLCanvas* assemble_canvas = assemble_view->get_wxglcanvas(); if (wxGetApp().is_editor()) { assemble_canvas->Bind(EVT_GLTOOLBAR_FILLCOLOR, [q](IntEvent& evt) { q->fill_color(evt.get_data()); }); @@ -6187,7 +7772,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_EXPORT_BEGAN, &priv::on_export_began, this); q->Bind(EVT_EXPORT_FINISHED, &priv::on_export_finished, this); q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); - //BBS: set on_slice to false + // BBS: set on_slice to false q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview", false); }); q->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); q->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); @@ -6208,14 +7793,14 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_PRINT_FINISHED, [q](wxCommandEvent& evt) { q->print_job_finished(evt); }); q->Bind(EVT_SEND_CALIBRATION_FINISHED, [q](wxCommandEvent& evt) { q->send_calibration_job_finished(evt); }); q->Bind(EVT_SEND_FINISHED, [q](wxCommandEvent& evt) { q->send_job_finished(evt); }); - q->Bind(EVT_PUBLISH_FINISHED, [q](wxCommandEvent& evt) { q->publish_job_finished(evt);}); - q->Bind(EVT_OPEN_PLATESETTINGSDIALOG, [q](wxCommandEvent& evt) { q->open_platesettings_dialog(evt);}); - q->Bind(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, [q](wxCommandEvent &evt) { q->open_filament_map_setting_dialog(evt); }); - //q->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); + q->Bind(EVT_PUBLISH_FINISHED, [q](wxCommandEvent& evt) { q->publish_job_finished(evt); }); + q->Bind(EVT_OPEN_PLATESETTINGSDIALOG, [q](wxCommandEvent& evt) { q->open_platesettings_dialog(evt); }); + q->Bind(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, [q](wxCommandEvent& evt) { q->open_filament_map_setting_dialog(evt); }); + // q->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); } // Drop target: - q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership + q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership q->Layout(); apply_color_mode(); @@ -6241,62 +7826,60 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // Register an USB HID (Human Interface Device) attach event. evt contains Win32 path to the USB device containing VID, PID and other info. // This event wakes up the Mouse3DController's background thread to enumerate HID devices, if the VID of the callback event // is one of the 3D Mouse vendors (3DConnexion or Logitech). - this->q->Bind(EVT_HID_DEVICE_ATTACHED, [this](HIDDeviceAttachedEvent &evt) { - mouse3d_controller.device_attached(evt.data); - }); - this->q->Bind(EVT_HID_DEVICE_DETACHED, [this](HIDDeviceAttachedEvent& evt) { - mouse3d_controller.device_detached(evt.data); - }); + this->q->Bind(EVT_HID_DEVICE_ATTACHED, [this](HIDDeviceAttachedEvent& evt) { mouse3d_controller.device_attached(evt.data); }); + this->q->Bind(EVT_HID_DEVICE_DETACHED, [this](HIDDeviceAttachedEvent& evt) { mouse3d_controller.device_detached(evt.data); }); #endif /* _WIN32 */ - //notification_manager = new NotificationManager(this->q); + // notification_manager = new NotificationManager(this->q); if (wxGetApp().is_editor()) { this->q->Bind(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, [this](EjectDriveNotificationClickedEvent&) { this->q->eject_drive(); }); this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); }); - this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, [](PresetUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); }); + this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, + [](PresetUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); }); this->q->Bind(EVT_PRINTER_CONFIG_UPDATE_AVAILABLE_CLICKED, [](PrinterConfigUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->do_printer_config_update(); - wxGetApp().getDeviceManager()->reload_printer_settings(); }); + wxGetApp().getDeviceManager()->reload_printer_settings(); + }); /* BBS do not handle removeable driver event */ - this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent &evt) { + this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent& evt) { if (evt.data.second) { // BBS - //this->show_action_buttons(this->ready_to_slice); + // this->show_action_buttons(this->ready_to_slice); notification_manager->close_notification_of_type(NotificationType::ExportFinished); - notification_manager->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotificationLevel, - format(_L("Successfully unmounted. The device %s (%s) can now be safely removed from the computer."), evt.data.first.name, evt.data.first.path) - ); + notification_manager->push_notification( + NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Successfully unmounted. The device %s (%s) can now be safely removed from the computer."), + evt.data.first.name, evt.data.first.path)); } else { notification_manager->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, - format(_L("Ejecting of device %s (%s) has failed."), evt.data.first.name, evt.data.first.path) - ); + format(_L("Ejecting of device %s (%s) has failed."), evt.data.first.name, + evt.data.first.path)); } }); - this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent &) { + this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent&) { // BBS - //this->show_action_buttons(this->ready_to_slice); + // this->show_action_buttons(this->ready_to_slice); // Close notification ExportingFinished but only if last export was to removable notification_manager->device_ejected(); }); // Start the background thread and register this window as a target for update events. wxGetApp().removable_drive_manager()->init(this->q); #ifdef _WIN32 - //Trigger enumeration of removable media on Win32 notification. - this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); - this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + // Trigger enumeration of removable media on Win32 notification. + this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent& evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent& evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); #endif /* _WIN32 */ } // Initialize the Undo / Redo stack with a first snapshot. - //this->take_snapshot("New Project", UndoRedo::SnapshotType::ProjectSeparator); + // this->take_snapshot("New Project", UndoRedo::SnapshotType::ProjectSeparator); // Reset the "dirty project" flag. m_undo_redo_stack_main.mark_current_as_saved(); dirty_state.update_from_undo_redo_stack(false); - //this->take_snapshot("New Project"); - // BBS: save project confirm + // this->take_snapshot("New Project"); + // BBS: save project confirm up_to_date(true, false); up_to_date(true, true); model.set_need_backup(); @@ -6308,7 +7891,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) std::string last_backup = last; std::string originfile; if (Slic3r::has_restore_data(last_backup, originfile)) { - auto result = MessageDialog(this->q, _L("Previously unsaved items have been detected. Do you want to restore them\?"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Restore"), wxYES_NO | wxYES_DEFAULT | wxCENTRE).ShowModal(); + auto result = MessageDialog(this->q, _L("Previously unsaved items have been detected. Do you want to restore them\?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Restore"), wxYES_NO | wxYES_DEFAULT | wxCENTRE) + .ShowModal(); if (result == wxID_YES) { this->q->load_project(from_path(last_backup), from_path(originfile)); Slic3r::backup_soon(); @@ -6322,13 +7907,13 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) } catch (...) {} - + if (this->q->get_project_filename().IsEmpty() && this->q->is_empty_project()) { int skip_confirm = e.GetInt(); this->q->new_project(skip_confirm, true); } }); - //wxPostEvent(this->q, wxCommandEvent{EVT_RESTORE_PROJECT}); + // wxPostEvent(this->q, wxCommandEvent{EVT_RESTORE_PROJECT}); } this->q->Bind(EVT_LOAD_MODEL_OTHER_INSTANCE, [this](LoadFromOtherInstanceEvent& evt) { @@ -6341,7 +7926,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) wxGetApp().mainframe->Raise(); this->q->load_files(input_files); }); - + this->q->Bind(EVT_START_DOWNLOAD_OTHER_INSTANCE, [](StartDownloadOtherInstanceEvent& evt) { BOOST_LOG_TRIVIAL(trace) << "Received url from other instance event."; wxGetApp().mainframe->Show(); @@ -6349,15 +7934,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) for (size_t i = 0; i < evt.data.size(); ++i) { wxGetApp().start_download(evt.data[i]); } - - }); - this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { - bring_instance_forward(); }); + this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent&) { bring_instance_forward(); }); wxGetApp().other_instance_message_handler()->init(this->q); // collapse sidebar according to saved value - //if (wxGetApp().is_editor()) { + // if (wxGetApp().is_editor()) { // bool is_collapsed = wxGetApp().app_config->get("collapsed_sidebar") == "1"; // sidebar->collapse(is_collapsed); //} @@ -6382,16 +7964,18 @@ void Plater::priv::update(unsigned int flags) model.center_instances_around_point(this->bed.build_volume().bed_center()); #endif - unsigned int update_status = 0; - const bool force_background_processing_restart = this->printer_technology == ptSLA || (flags & (unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); + unsigned int update_status = 0; + const bool force_background_processing_restart = this->printer_technology == ptSLA || + (flags & (unsigned int) UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); if (force_background_processing_restart) // Update the SLAPrint from the current Model, so that the reload_scene() // pulls the correct data. - update_status = this->update_background_process(false, flags & (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); - //BBS TODO reload_scene - this->view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH); - if (is_preview_shown()) this->preview->reload_print(); - //BBS assemble view + update_status = this->update_background_process(false, flags & (unsigned int) UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + // BBS TODO reload_scene + this->view3D->reload_scene(false, flags & (unsigned int) UpdateParams::FORCE_FULL_SCREEN_REFRESH); + if (is_preview_shown()) + this->preview->reload_print(); + // BBS assemble view this->assemble_view->reload_scene(false, flags); // todo: better to mark thumbnail dirty here @@ -6417,25 +8001,23 @@ void Plater::priv::select_view(const std::string& direction) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; view3D->select_view(direction); wxGetApp().update_ui_from_settings(); - } - else if (current_panel == preview) { + } else if (current_panel == preview) { BOOST_LOG_TRIVIAL(info) << "select preview"; preview->select_view(direction); wxGetApp().update_ui_from_settings(); - } - else if (current_panel == assemble_view) { + } else if (current_panel == assemble_view) { BOOST_LOG_TRIVIAL(info) << "select assemble view"; assemble_view->select_view(direction); } } -const VendorProfile::PrinterModel *Plater::get_curr_printer_model() +const VendorProfile::PrinterModel* Plater::get_curr_printer_model() { auto bundle = wxGetApp().preset_bundle; if (bundle) { - const Preset *curr = &bundle->printers.get_selected_preset(); + const Preset* curr = &bundle->printers.get_selected_preset(); if (curr) { - const VendorProfile::PrinterModel *pm = PresetUtils::system_printer_model(*curr); + const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(*curr); if (!pm) { auto curr_parent = bundle->printers.get_selected_preset_parent(); if (curr_parent) { @@ -6475,9 +8057,9 @@ std::map Plater::get_bed_texture_maps() bool Plater::get_enable_wrapping_detection() { - const DynamicPrintConfig & print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const ConfigOptionBool * wrapping_detection = print_config.option("enable_wrapping_detection"); - return (wrapping_detection != nullptr) && wrapping_detection->value; + const DynamicPrintConfig& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const ConfigOptionBool* wrapping_detection = print_config.option("enable_wrapping_detection"); + return (wrapping_detection != nullptr) && wrapping_detection->value; } wxColour Plater::get_next_color_for_filament() @@ -6486,22 +8068,9 @@ wxColour Plater::get_next_color_for_filament() // refs to https://www.ebaomonthly.com/window/photo/lesson/colorList.htm wxColour colors[FILAMENT_SYSTEM_COLORS_NUM] = { // ORCA updated all color palette - wxColour("#00C1AE"), - wxColour("#F4E2C1"), - wxColour("#ED1C24"), - wxColour("#00FF7F"), - wxColour("#F26722"), - wxColour("#FFEB31"), - wxColour("#7841CE"), - wxColour("#115877"), - wxColour("#ED1E79"), - wxColour("#2EBDEF"), - wxColour("#345B2F"), - wxColour("#800080"), - wxColour("#FA8173"), - wxColour("#800000"), - wxColour("#F7B763"), - wxColour("#A4C41E"), + wxColour("#00C1AE"), wxColour("#F4E2C1"), wxColour("#ED1C24"), wxColour("#00FF7F"), wxColour("#F26722"), wxColour("#FFEB31"), + wxColour("#7841CE"), wxColour("#115877"), wxColour("#ED1E79"), wxColour("#2EBDEF"), wxColour("#345B2F"), wxColour("#800080"), + wxColour("#FA8173"), wxColour("#800000"), wxColour("#F7B763"), wxColour("#A4C41E"), }; return colors[curr_color_filamenet++ % FILAMENT_SYSTEM_COLORS_NUM]; } @@ -6509,22 +8078,25 @@ wxColour Plater::get_next_color_for_filament() wxString Plater::get_slice_warning_string(GCodeProcessorResult::SliceWarning& warning) { if (warning.msg == BED_TEMP_TOO_HIGH_THAN_FILAMENT) { - return _L("The current heatbed temperature is relatively high. The nozzle may clog when printing this filament in a closed environment. Please open the front door and/or remove the upper glass."); + return _L("The current heatbed temperature is relatively high. The nozzle may clog when printing this filament in a closed " + "environment. Please open the front door and/or remove the upper glass."); } else if (warning.msg == NOZZLE_HRC_CHECKER) { - return _L("The nozzle hardness required by the filament is higher than the default nozzle hardness of the printer. Please replace the hardened nozzle or filament, otherwise, the nozzle will be worn down or damaged."); + return _L("The nozzle hardness required by the filament is higher than the default nozzle hardness of the printer. Please replace " + "the hardened nozzle or filament, otherwise, the nozzle will be worn down or damaged."); } else if (warning.msg == NOT_SUPPORT_TRADITIONAL_TIMELAPSE) { - return _L("Enabling traditional timelapse photography may cause surface imperfections. It is recommended to change to smooth mode."); + return _L( + "Enabling traditional timelapse photography may cause surface imperfections. It is recommended to change to smooth mode."); } else if (warning.msg == NOT_GENERATE_TIMELAPSE) { return wxString(); } else if (warning.msg == SMOOTH_TIMELAPSE_WITHOUT_PRIME_TOWER) { - return _L("Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again."); - } - else { + return _L("Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the " + "prime tower, re-slice and print again."); + } else { return wxString(warning.msg); } } -void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) +void Plater::priv::apply_free_camera_correction(bool apply /* = true*/) { bool use_perspective_camera = get_config("use_perspective_camera").compare("true") == 0; if (use_perspective_camera) @@ -6535,7 +8107,7 @@ void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) camera.recover_from_free_camera(); } -//BBS: add no slice option +// BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { if (name == "3D") { @@ -6544,25 +8116,23 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("goto preview page when loading gcode/exported_3mf"); } set_current_panel(view3D, no_slice); - } - else if (name == "Preview") { + } else if (name == "Preview") { BOOST_LOG_TRIVIAL(info) << "select preview"; - //BBS update extruder params and speed table before slicing + // BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); - auto& print = q->get_partplate_list().get_current_fff_print(); - auto print_config = print.config(); - int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); set_current_panel(preview, no_slice); - } - else if (name == "Assemble") { + } else if (name == "Assemble") { BOOST_LOG_TRIVIAL(info) << "select assemble view"; set_current_panel(assemble_view, no_slice); } - //BBS update selection + // BBS update selection wxGetApp().obj_list()->update_selections(); selection_changed(); @@ -6571,13 +8141,12 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) void Plater::priv::select_next_view_3D() { - if (current_panel == view3D) wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); else if (current_panel == preview) wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); -// else if (current_panel == assemble_view) -// set_current_panel(view3D); + // else if (current_panel == assemble_view) + // set_current_panel(view3D); } void Plater::priv::enable_sidebar(bool enabled) @@ -6597,9 +8166,7 @@ void Plater::priv::collapse_sidebar(bool collapse) sidebar_layout.is_collapsed = collapse; // Now update the tooltip in the toolbar. - std::string new_tooltip = collapse - ? _u8L("Expand sidebar") - : _u8L("Collapse sidebar"); + std::string new_tooltip = collapse ? _u8L("Expand sidebar") : _u8L("Collapse sidebar"); new_tooltip += " [" + _u8L("Shift+") + _u8L("Tab") + "]"; int id = collapse_toolbar.get_item_id("collapse_sidebar"); collapse_toolbar.set_tooltip(id, new_tooltip); @@ -6607,12 +8174,13 @@ void Plater::priv::collapse_sidebar(bool collapse) update_sidebar(); } -void Plater::priv::update_sidebar(bool force_update) { +void Plater::priv::update_sidebar(bool force_update) +{ auto& sidebar = m_aui_mgr.GetPane(this->sidebar); if (!sidebar.IsOk() || this->current_panel == nullptr) { return; } - bool needs_update = force_update; + bool needs_update = force_update; if (!sidebar_layout.is_enabled) { if (sidebar.IsShown()) { @@ -6642,23 +8210,21 @@ void Plater::priv::reset_window_layout() update_sidebar(true); } -Sidebar::DockingState Plater::priv::get_sidebar_docking_state() { +Sidebar::DockingState Plater::priv::get_sidebar_docking_state() +{ if (!sidebar_layout.is_enabled) { return Sidebar::None; } const auto& sidebar = m_aui_mgr.GetPane(this->sidebar); - if(sidebar.IsFloating()) { + if (sidebar.IsFloating()) { return Sidebar::None; } return sidebar.dock_direction == wxAUI_DOCK_RIGHT ? Sidebar::Right : Sidebar::Left; } -void Plater::priv::reset_all_gizmos() -{ - view3D->get_canvas3d()->reset_all_gizmos(); -} +void Plater::priv::reset_all_gizmos() { view3D->get_canvas3d()->reset_all_gizmos(); } // Called after the Preferences dialog is closed and the program settings are saved. // Update the UI based on the current preferences. @@ -6673,15 +8239,9 @@ void Plater::priv::update_ui_from_settings() } // BBS -std::shared_ptr Plater::priv::statusbar() -{ - return nullptr; -} +std::shared_ptr Plater::priv::statusbar() { return nullptr; } -std::string Plater::priv::get_config(const std::string &key) const -{ - return wxGetApp().app_config->get(key); -} +std::string Plater::priv::get_config(const std::string& key) const { return wxGetApp().app_config->get(key); } BoundingBoxf Plater::priv::bed_shape_bb() const { @@ -6691,14 +8251,14 @@ BoundingBoxf Plater::priv::bed_shape_bb() const BoundingBox Plater::priv::scaled_bed_shape_bb() const { - const auto *bed_shape_opt = config->opt("printable_area"); + const auto* bed_shape_opt = config->opt("printable_area"); const auto printable_area = Slic3r::Polygon::new_scale(bed_shape_opt->values); return printable_area.bounding_box(); } - -void read_binary_stl(const std::string& filename, std::string& model_id, std::string& code) { - std::ifstream file( encode_path(filename.c_str()), std::ios::binary); +void read_binary_stl(const std::string& filename, std::string& model_id, std::string& code) +{ + std::ifstream file(encode_path(filename.c_str()), std::ios::binary); if (!file) { return; } @@ -6717,7 +8277,7 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st return; } - char magic[2] = { data[0], data[1] }; + char magic[2] = {data[0], data[1]}; if (magic[0] != 'M' || magic[1] != 'W') { file.close(); return; @@ -6728,9 +8288,9 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st return; } - char protocol_version[3] = { data[3], data[4], data[5] }; + char protocol_version[3] = {data[3], data[4], data[5]}; - //version + // version if (protocol_version[0] != '1' || protocol_version[1] != '.' || protocol_version[2] != '0') { file.close(); return; @@ -6745,45 +8305,45 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st tokens.push_back(tokenPtr); } - //model id + // model id if (tokens.size() < 4) { file.close(); return; } model_id = tokens[2]; - code = tokens[3]; + code = tokens[3]; file.close(); - } - catch (...) { - } + } catch (...) {} return; } // BBS: backup & restore -std::vector Plater::priv::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) +std::vector Plater::priv::load_files(const std::vector& input_files, + LoadStrategy strategy, + bool ask_multi, + bool* published_out) { std::vector empty_result; - bool dlg_cont = true; + bool dlg_cont = true; bool is_user_cancel = false; - bool translate_old = false; + bool translate_old = false; int current_width = 0, current_depth = 0, current_height = 0, project_filament_count = 1; if (input_files.empty()) return std::vector(); if (!input_files.empty()) - q->m_3mf_path = input_files[0].string(); - + q->m_3mf_path = input_files[0].string(); + // SoftFever: ugly fix so we can exist pa calib mode background_process.fff_print()->calib_mode() = CalibMode::Calib_None; - // BBS int filaments_cnt = config->opt("filament_colour")->values.size(); - bool one_by_one = input_files.size() == 1 || printer_technology == ptSLA/* || filaments_cnt <= 1*/; - if (! one_by_one) { - for (const auto &path : input_files) { + bool one_by_one = input_files.size() == 1 || printer_technology == ptSLA /* || filaments_cnt <= 1*/; + if (!one_by_one) { + for (const auto& path : input_files) { if (std::regex_match(path.string(), pattern_bundle)) { one_by_one = true; break; @@ -6791,75 +8351,75 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - bool load_model = strategy & LoadStrategy::LoadModel; - bool load_config = strategy & LoadStrategy::LoadConfig; + bool load_model = strategy & LoadStrategy::LoadModel; + bool load_config = strategy & LoadStrategy::LoadConfig; bool imperial_units = strategy & LoadStrategy::ImperialUnits; - bool silence = strategy & LoadStrategy::Silence; + bool silence = strategy & LoadStrategy::Silence; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": load_model %1%, load_config %2%, input_files size %3%")%load_model %load_config %input_files.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": load_model %1%, load_config %2%, input_files size %3%") % load_model % load_config % + input_files.size(); const auto loading = _L("Loading") + dots; ProgressDialog dlg(loading, "", 100, find_toplevel_parent(q), wxPD_AUTO_HIDE | wxPD_CAN_ABORT | wxPD_APP_MODAL); wxBusyCursor busy; - auto *new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); + auto* new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); std::vector obj_idxs; - std::string designer_model_id; - std::string designer_country_code; + std::string designer_model_id; + std::string designer_country_code; - int answer_convert_from_meters = wxOK_DEFAULT; - int answer_convert_from_imperial_units = wxOK_DEFAULT; - int tolal_model_count = 0; + int answer_convert_from_meters = wxOK_DEFAULT; + int answer_convert_from_imperial_units = wxOK_DEFAULT; + int tolal_model_count = 0; - int progress_percent = 0; - int total_files = input_files.size(); - const int stage_percent[IMPORT_STAGE_MAX+1] = { - 5, // IMPORT_STAGE_RESTORE - 10, // IMPORT_STAGE_OPEN - 30, // IMPORT_STAGE_READ_FILES - 50, // IMPORT_STAGE_EXTRACT - 60, // IMPORT_STAGE_LOADING_OBJECTS - 70, // IMPORT_STAGE_LOADING_PLATES - 80, // IMPORT_STAGE_FINISH - 85, // IMPORT_STAGE_ADD_INSTANCE - 90, // IMPORT_STAGE_UPDATE_GCODE - 92, // IMPORT_STAGE_CHECK_MODE_GCODE - 95, // UPDATE_GCODE_RESULT - 98, // IMPORT_LOAD_CONFIG - 99, // IMPORT_LOAD_MODEL_OBJECTS - 100 - }; - const int step_percent[LOAD_STEP_STAGE_NUM+1] = { - 5, // LOAD_STEP_STAGE_READ_FILE - 30, // LOAD_STEP_STAGE_GET_SOLID - 60, // LOAD_STEP_STAGE_GET_MESH - 100 - }; + int progress_percent = 0; + int total_files = input_files.size(); + const int stage_percent[IMPORT_STAGE_MAX + 1] = {5, // IMPORT_STAGE_RESTORE + 10, // IMPORT_STAGE_OPEN + 30, // IMPORT_STAGE_READ_FILES + 50, // IMPORT_STAGE_EXTRACT + 60, // IMPORT_STAGE_LOADING_OBJECTS + 70, // IMPORT_STAGE_LOADING_PLATES + 80, // IMPORT_STAGE_FINISH + 85, // IMPORT_STAGE_ADD_INSTANCE + 90, // IMPORT_STAGE_UPDATE_GCODE + 92, // IMPORT_STAGE_CHECK_MODE_GCODE + 95, // UPDATE_GCODE_RESULT + 98, // IMPORT_LOAD_CONFIG + 99, // IMPORT_LOAD_MODEL_OBJECTS + 100}; + const int step_percent[LOAD_STEP_STAGE_NUM + 1] = {5, // LOAD_STEP_STAGE_READ_FILE + 30, // LOAD_STEP_STAGE_GET_SOLID + 60, // LOAD_STEP_STAGE_GET_MESH + 100}; - const float INPUT_FILES_RATIO = 0.7; - const float INIT_MODEL_RATIO = 0.75; - const float CENTER_AROUND_ORIGIN_RATIO = 0.8; - const float LOAD_MODEL_RATIO = 0.9; + const float INPUT_FILES_RATIO = 0.7; + const float INIT_MODEL_RATIO = 0.75; + const float CENTER_AROUND_ORIGIN_RATIO = 0.8; + const float LOAD_MODEL_RATIO = 0.9; for (size_t i = 0; i < input_files.size(); ++i) { int file_percent = 0; #ifdef _WIN32 auto path = input_files[i]; - // On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it assignes full path to names of loaded objects. + // On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it + // assignes full path to names of loaded objects. path.make_preferred(); #else // _WIN32 // Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute. - const auto &path = input_files[i]; + const auto& path = input_files[i]; #endif // _WIN32 - const auto filename = path.filename(); - int progress_percent = static_cast(100.0f * static_cast(i) / static_cast(input_files.size())); - const auto real_filename = (strategy & LoadStrategy::Restore) ? input_files[++i].filename() : filename; - const auto dlg_info = _L("Loading file") + ": " + from_path(real_filename); + const auto filename = path.filename(); + int progress_percent = static_cast(100.0f * static_cast(i) / static_cast(input_files.size())); + const auto real_filename = (strategy & LoadStrategy::Restore) ? input_files[++i].filename() : filename; + const auto dlg_info = _L("Loading file") + ": " + from_path(real_filename); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << boost::format(": load file %1%") % filename; dlg_cont = dlg.Update(progress_percent, dlg_info); - if (!dlg_cont) return empty_result; + if (!dlg_cont) + return empty_result; const bool type_3mf = std::regex_match(path.string(), pattern_3mf); // const bool type_zip_amf = !type_3mf && std::regex_match(path.string(), pattern_zip_amf); @@ -6873,14 +8433,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ load_aux = true; strategy = strategy | LoadStrategy::LoadAuxiliary; } - if (load_config) strategy = strategy | LoadStrategy::CheckVersion; + if (load_config) + strategy = strategy | LoadStrategy::CheckVersion; bool is_project_file = false; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": is_project_file %1%, type_3mf %2%") % is_project_file % type_3mf; try { if (type_3mf) { DynamicPrintConfig config; - Semver file_version; - En3mfType en_3mf_file_type = En3mfType::From_BBS; + Semver file_version; + En3mfType en_3mf_file_type = En3mfType::From_BBS; // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; // on load keep the user's current presets and overlay only those keys. Declared // here (outside the config block below) so it stays alive for the embedded-preset @@ -6890,28 +8451,43 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicPrintConfig config_loaded; // BBS: add part plate related logic - PlateDataPtrs plate_data; + PlateDataPtrs plate_data; ConfigSubstitutionContext config_substitutions{ForwardCompatibilitySubstitutionRule::Enable}; - std::vector project_presets; + std::vector project_presets; // BBS: backup & restore q->skip_thumbnail_invalid = true; - model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, en_3mf_file_type, strategy, &plate_data, &project_presets, - &file_version, - [this, &dlg, real_filename, &progress_percent, &file_percent, stage_percent, INPUT_FILES_RATIO, total_files, i, - &is_user_cancel](int import_stage, int current, int total, bool &cancel) { - bool cont = true; - float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * ((float)stage_percent[import_stage] + (float)current * (float)(stage_percent[import_stage + 1] - stage_percent[import_stage]) /(float) total) / (float)total_files; - BOOST_LOG_TRIVIAL(trace) << "load_3mf_file: percent(float)=" << percent_float << ", stage = " << import_stage << ", curr = " << current << ", total = " << total; - progress_percent = (int)percent_float; - wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); - cont = dlg.Update(progress_percent, msg); - cancel = !cont; + model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, en_3mf_file_type, + strategy, &plate_data, &project_presets, &file_version, + [this, &dlg, real_filename, &progress_percent, &file_percent, stage_percent, + INPUT_FILES_RATIO, total_files, i, + &is_user_cancel](int import_stage, int current, int total, bool& cancel) { + bool cont = true; + float percent_float = (100.0f * (float) i / (float) total_files) + + INPUT_FILES_RATIO * + ((float) stage_percent[import_stage] + + (float) current * + (float) (stage_percent[import_stage + 1] - + stage_percent[import_stage]) / + (float) total) / + (float) total_files; + BOOST_LOG_TRIVIAL(trace) + << "load_3mf_file: percent(float)=" << percent_float + << ", stage = " << import_stage << ", curr = " << current + << ", total = " << total; + progress_percent = (int) percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), + from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; if (cancel) is_user_cancel = cancel; }); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ - << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() % - project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string(); + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << ":" << __LINE__ + << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % + plate_data.size() % project_presets.size() % + (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % + file_version.to_string(); // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; // on load keep the user's current presets and overlay only those keys. Parsed @@ -6923,12 +8499,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto published_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG); if (published_it != model.model_info->metadata_items.end() && is_published_3mf_flag(published_it->second)) { published_config.published = true; - auto keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG); + auto keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG); if (keys_it != model.model_info->metadata_items.end()) { try { auto j = nlohmann::json::parse(keys_it->second); if (j.is_array()) - for (const auto &k : j) + for (const auto& k : j) if (k.is_string()) published_config.published_keys.emplace_back(k.get()); } catch (...) { @@ -6941,7 +8517,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ try { auto jm = nlohmann::json::parse(material_keys_it->second); if (jm.is_array()) - for (const auto &m : jm) { + for (const auto& m : jm) { try { // Malformed entries are isolated so one bad item // cannot discard valid entries that follow it. @@ -6950,7 +8526,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ PublishedMaterialEntry entry; const auto mat_it = m.find("material"); if (mat_it != m.end() && mat_it->is_object()) { - const auto &mat = *mat_it; + const auto& mat = *mat_it; if (mat.contains("filament_type") && mat["filament_type"].is_string()) entry.filament_type = mat["filament_type"].get(); if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string()) @@ -6966,7 +8542,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ entry.slot = m["slot"].get(); const auto entry_keys_it = m.find("keys"); if (entry_keys_it != m.end() && entry_keys_it->is_array()) - for (const auto &k : *entry_keys_it) + for (const auto& k : *entry_keys_it) if (k.is_string()) entry.keys.emplace_back(k.get()); // Fields always written by the current exporter. @@ -6974,7 +8550,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ entry.full = m["full"].get(); const auto entry_full_keys_it = m.find("full_keys"); if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array()) - for (const auto &k : *entry_full_keys_it) + for (const auto& k : *entry_full_keys_it) if (k.is_string()) entry.full_keys.emplace_back(k.get()); if (m.contains("publish_type") && m["publish_type"].is_boolean()) @@ -6986,11 +8562,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (m.contains("color") && m["color"].is_string()) entry.color = m["color"].get(); published_config.material_keys.emplace_back(std::move(entry)); - } catch (const nlohmann::json::exception &) { + } catch (const nlohmann::json::exception&) { // Ignore only this malformed material entry. } } - } catch (const nlohmann::json::exception &) { + } catch (const nlohmann::json::exception&) { // Ignore malformed published_material_keys; the project still loads normally. } } @@ -7002,7 +8578,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto payload_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG); if (payload_it != model.model_info->metadata_items.end()) { try { - ConfigSubstitutions payload_substitutions = config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable); + ConfigSubstitutions payload_substitutions = + config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable); config_substitutions.substitutions.insert(config_substitutions.substitutions.end(), std::make_move_iterator(payload_substitutions.begin()), std::make_move_iterator(payload_substitutions.end())); @@ -7017,20 +8594,24 @@ std::vector Plater::priv::load_files(const std::vector& input_ // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { std::set extruderIds; - for (ModelObject *o : model.objects) { - if (o->config.option("extruder")) extruderIds.insert(o->config.extruder()); + for (ModelObject* o : model.objects) { + if (o->config.option("extruder")) + extruderIds.insert(o->config.extruder()); for (auto volume : o->volumes) { - if (volume->config.option("extruder")) extruderIds.insert(volume->config.extruder()); - for (int extruder : volume->get_extruders()) { extruderIds.insert(extruder); } + if (volume->config.option("extruder")) + extruderIds.insert(volume->config.extruder()); + for (int extruder : volume->get_extruders()) { + extruderIds.insert(extruder); + } } } int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); int filament_size = sidebar->combos_filament().size(); while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { - int filament_count = filament_size + 1; - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + int filament_count = filament_size + 1; + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().plater()->on_filament_count_change(filament_count); ++filament_size; @@ -7043,21 +8624,22 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (import_project_action.empty()) load_type = LoadType::Unknown; else - load_type = static_cast(std::stoi(import_project_action)); + load_type = static_cast(std::stoi(import_project_action)); // BBS: version check - Semver app_version = *(Semver::parse(SoftFever_VERSION)); - const wxString load_3mf_title = _L("Load 3MF"); - const wxString newer_3mf_title = _L("Newer 3MF version"); - const wxString bambu_project_title = _L("BambuStudio Project"); - const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only."); - const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only."); - const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only."); - const wxString msg_bambu_geometry = _L("The 3MF file was generated by BambuStudio, loading geometry data only."); - auto log_and_show_3mf_info = [&](const wxString& text, const wxString& title) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ - << " " - << boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string(); + Semver app_version = *(Semver::parse(SoftFever_VERSION)); + const wxString load_3mf_title = _L("Load 3MF"); + const wxString newer_3mf_title = _L("Newer 3MF version"); + const wxString bambu_project_title = _L("BambuStudio Project"); + const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only."); + const wxString msg_old_orca_geometry = _L( + "The 3MF file was generated by an old OrcaSlicer version, loading geometry data only."); + const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only."); + const wxString msg_bambu_geometry = _L("The 3MF file was generated by BambuStudio, loading geometry data only."); + auto log_and_show_3mf_info = [&](const wxString& text, const wxString& title) { + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << ":" << __LINE__ << " " + << boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string(); show_info(q, text, title); }; if (en_3mf_file_type == En3mfType::From_Prusa) { @@ -7065,8 +8647,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ load_config = false; if (load_type != LoadType::LoadGeometry) log_and_show_3mf_info(msg_unsupported_geometry, load_3mf_title); - } - else if (en_3mf_file_type == En3mfType::From_Orca) { + } else if (en_3mf_file_type == En3mfType::From_Orca) { // OrcaSlicer file (has OrcaSlicer tag) - compare file_version with SoftFever_VERSION // Migration fix for OrcaSlicer 2.3.1-alpha sparse infill rotation template if (load_config && (file_version < app_version) && file_version == Semver("2.3.1-alpha")) { @@ -7081,8 +8662,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " "infill rotation template settings that may not work properly with your current infill pattern. " "This could result in weak support or print quality issues.")); - msg_text += "\n\n" + - _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?")); + msg_text += "\n\n" + _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation " + "template settings?")); MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); dialog.SetButtonLabel(wxID_YES, _L("Yes")); dialog.SetButtonLabel(wxID_NO, _L("No")); @@ -7093,31 +8674,30 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else if (load_config && (file_version > app_version)) { if (config_substitutions.unrecogized_keys.size() > 0) { - wxString text = wxString::Format(_L("The 3MF file version %s is newer than %s's version %s, found the following unrecognized keys:"), - file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); + wxString text = wxString::Format( + _L("The 3MF file version %s is newer than %s's version %s, found the following unrecognized keys:"), + file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); text += "\n"; wxString context = text; - wxString append = _L("You should update your software.\n"); + wxString append = _L("You should update your software.\n"); context += "\n\n"; context += append; log_and_show_3mf_info(context, newer_3mf_title); - } - else { - //if the minor version is not matched + } else { + // if the minor version is not matched if (file_version.min() != app_version.min()) { - wxString text = wxString::Format(_L("The 3MF file version %s is newer than %s's version %s, we suggest to upgrade your software."), - file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); + wxString text = wxString::Format( + _L("The 3MF file version %s is newer than %s's version %s, we suggest to upgrade your software."), + file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); text += "\n"; log_and_show_3mf_info(text, newer_3mf_title); } } - } - else if (load_config && config_loaded.empty()) { + } else if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_old_orca_geometry, load_3mf_title); } - } - else if (en_3mf_file_type == En3mfType::From_BBS) { + } else if (en_3mf_file_type == En3mfType::From_BBS) { // No OrcaSlicer tag - check Bambu/Application version Semver orca_tag_start_version(2, 3, 2); if (file_version <= orca_tag_start_version) { @@ -7132,12 +8712,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ _sparse_infill_pattern == ipZigZag || _sparse_infill_pattern == ipCrossZag || _sparse_infill_pattern == ipLockedZag; if (!is_safe_to_rotate) { - wxString msg_text = _( - L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " - "infill rotation template settings that may not work properly with your current infill pattern. " - "This could result in weak support or print quality issues.")); - msg_text += "\n\n" + - _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?")); + wxString msg_text = _(L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " + "infill rotation template settings that may not work properly with your " + "current infill pattern. " + "This could result in weak support or print quality issues.")); + msg_text += "\n\n" + _(L("Would you like OrcaSlicer to automatically fix this by clearing the " + "rotation template settings?")); MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); dialog.SetButtonLabel(wxID_YES, _L("Yes")); dialog.SetButtonLabel(wxID_NO, _L("No")); @@ -7146,8 +8726,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } } - } - else if (load_config && config_loaded.empty()) { + } else if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_older_geometry, load_3mf_title); } @@ -7158,21 +8737,23 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title); - } - else if (load_config && (file_version > slic3r_version)) { + } else if (load_config && (file_version > slic3r_version)) { // BambuStudio file version is newer than our compatible SLIC3R_VERSION if (config_substitutions.unrecogized_keys.size() > 0) { - wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"), + wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer " + "than the compatible version %s. Found unrecognized settings:"), file_version.to_string(), slic3r_version.to_string()); text += "\n"; wxString context = text; - wxString append = _L("You should update your software.\n"); + wxString append = _L("You should update your software.\n"); context += "\n\n"; context += append; log_and_show_3mf_info(context, bambu_project_title); } else { - wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."), - file_version.to_string(), slic3r_version.to_string()); + wxString text = + wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the " + "compatible version %s. Some settings may not be fully compatible."), + file_version.to_string(), slic3r_version.to_string()); text += "\n"; log_and_show_3mf_info(text, bambu_project_title); } @@ -7182,41 +8763,46 @@ std::vector Plater::priv::load_files(const std::vector& input_ log_and_show_3mf_info(text, bambu_project_title); } } - } - else if (en_3mf_file_type == En3mfType::From_Other) { + } else if (en_3mf_file_type == En3mfType::From_Other) { // Generic CAD/other 3MF without slicer metadata: import geometry silently. if (load_config && config_loaded.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ - << " " - << boost::format("3MF has no slicer metadata/project config, importing geometry only: %1%") % path.string(); + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << ":" << __LINE__ << " " + << boost::format("3MF has no slicer metadata/project config, importing geometry only: %1%") % path.string(); load_config = false; } - } - else if (load_config && config_loaded.empty()) { + } else if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_old_orca_geometry, load_3mf_title); - } - else if (!load_config) { + } else if (!load_config) { // reset config except color - for (ModelObject *model_object : model.objects) { + for (ModelObject* model_object : model.objects) { bool has_extruder = model_object->config.has("extruder"); - int extruder_id = -1; + int extruder_id = -1; // save the extruder information before reset - if (has_extruder) { extruder_id = model_object->config.extruder(); } + if (has_extruder) { + extruder_id = model_object->config.extruder(); + } model_object->config.reset(); // restore the extruder after reset - if (has_extruder) { model_object->config.set("extruder", extruder_id); } + if (has_extruder) { + model_object->config.set("extruder", extruder_id); + } // Is there any modifier or advanced config data? - for (ModelVolume *model_volume : model_object->volumes) { + for (ModelVolume* model_volume : model_object->volumes) { has_extruder = model_volume->config.has("extruder"); - if (has_extruder) { extruder_id = model_volume->config.extruder(); } + if (has_extruder) { + extruder_id = model_volume->config.extruder(); + } model_volume->config.reset(); - if (has_extruder) { model_volume->config.set("extruder", extruder_id); } + if (has_extruder) { + model_volume->config.set("extruder", extruder_id); + } } } } @@ -7236,15 +8822,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ } Semver old_version(1, 5, 9); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && + (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { translate_old = true; partplate_list.get_plate_size(current_width, current_depth, current_height); } if (load_config) { if (translate_old) { - //set the size back - partplate_list.reset_size(current_width + Bed3D::Axes::DefaultTipRadius, current_depth + Bed3D::Axes::DefaultTipRadius, current_height, false); + // set the size back + partplate_list.reset_size(current_width + Bed3D::Axes::DefaultTipRadius, + current_depth + Bed3D::Axes::DefaultTipRadius, current_height, false); } project_filament_count = config_loaded.option("filament_colour")->size(); partplate_list.load_from_3mf_structure(plate_data, project_filament_count); @@ -7264,12 +8852,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ if ((project_presets.size() > 0) && load_config && !published_config.published) { // load project embedded presets PresetsConfigSubstitutions preset_substitutions; - PresetBundle & preset_bundle = *wxGetApp().preset_bundle; - preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); - if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, + ForwardCompatibilitySubstitutionRule::Enable); + if (!preset_substitutions.empty()) + show_substitutions_info(preset_substitutions); } if (project_presets.size() > 0) { - for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } + for (unsigned int i = 0; i < project_presets.size(); i++) { + delete project_presets[i]; + } project_presets.clear(); } @@ -7285,35 +8877,37 @@ std::vector Plater::priv::load_files(const std::vector& input_ // Based on the printer technology field found in the loaded config, select the base for the config, PrinterTechnology printer_technology = Preset::printer_technology(config_loaded); - config.apply(static_cast(FullPrintConfig::defaults())); + config.apply(static_cast(FullPrintConfig::defaults())); // and place the loaded config over the base. config += std::move(config_loaded); std::map validity = config.validate(); if (!validity.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("Param values in 3mf error: "); - for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("%1%: %2%")%it->first %it->second; + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("Param values in 3mf error: "); + for (std::map::iterator it = validity.begin(); it != validity.end(); ++it) + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("%1%: %2%") % it->first % it->second; // - NotificationManager *notify_manager = q->get_notification_manager(); - std::string error_message = _u8L("Invalid values found in the 3MF:"); + NotificationManager* notify_manager = q->get_notification_manager(); + std::string error_message = _u8L("Invalid values found in the 3MF:"); error_message += "\n"; - for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) + for (std::map::iterator it = validity.begin(); it != validity.end(); ++it) error_message += "-" + it->first + ": " + it->second + "\n"; error_message += "\n"; error_message += _u8L("Please correct them in the Param tabs"); notify_manager->bbl_show_3mf_warn_notification(error_message); } } - if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); + if (!config_substitutions.empty()) + show_substitutions_info(config_substitutions.substitutions, filename.string()); // BBS if (load_model && !load_config) { ; - } - else { + } else { this->model.plates_custom_gcodes = model.plates_custom_gcodes; - this->model.design_info = model.design_info; - this->model.model_info = model.model_info; + this->model.design_info = model.design_info; + this->model.model_info = model.model_info; } } @@ -7333,52 +8927,60 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_config) { if (!config.empty()) { Preset::normalize(config); - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; { // BBS: modify the prime tower params for old version file Semver old_version3(2, 0, 0); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) { + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && + file_version < old_version3) { double old_filament_prime_volume = 0.; - int filament_count = 0; + int filament_count = 0; { - ConfigOptionFloats *filament_prime_volume_option = config.option("filament_prime_volume"); - ConfigOptionStrings *filament_colors_option = config.option("filament_colour", true); - filament_count = filament_colors_option->values.size(); + ConfigOptionFloats* filament_prime_volume_option = config.option( + "filament_prime_volume"); + ConfigOptionStrings* filament_colors_option = config.option("filament_colour", + true); + filament_count = filament_colors_option->values.size(); if (filament_prime_volume_option) { - std::vector &filament_prime_volume_values = filament_prime_volume_option->values; + std::vector& filament_prime_volume_values = filament_prime_volume_option->values; if (!filament_prime_volume_values.empty()) { old_filament_prime_volume = filament_prime_volume_values[0]; - if (filament_count > 1) filament_prime_volume_values.resize(filament_count, old_filament_prime_volume); + if (filament_count > 1) + filament_prime_volume_values.resize(filament_count, old_filament_prime_volume); } } } - ConfigOptionEnum *prime_tower_rib_wall_option = config.option>("wipe_tower_wall_type", true); - prime_tower_rib_wall_option->value = WipeTowerWallType::wtwRectangle; + ConfigOptionEnum* prime_tower_rib_wall_option = + config.option>("wipe_tower_wall_type", true); + prime_tower_rib_wall_option->value = WipeTowerWallType::wtwRectangle; - ConfigOptionPercent *prime_tower_infill_gap_option = config.option("prime_tower_infill_gap", true); - prime_tower_infill_gap_option->value = 100; + ConfigOptionPercent* prime_tower_infill_gap_option = + config.option("prime_tower_infill_gap", true); + prime_tower_infill_gap_option->value = 100; - ConfigOptionInts *filament_adhesiveness_category_option = config.option("filament_adhesiveness_category", true); - std::vector &filament_adhesiveness_category_values = filament_adhesiveness_category_option->values; + ConfigOptionInts* filament_adhesiveness_category_option = + config.option("filament_adhesiveness_category", true); + std::vector& filament_adhesiveness_category_values = filament_adhesiveness_category_option->values; filament_adhesiveness_category_values.resize(filament_count); for (int index = 0; index < filament_count; index++) filament_adhesiveness_category_values[index] = 100; - std::vector &diff_settings = config.option("different_settings_to_system", true)->values; + std::vector& diff_settings = + config.option("different_settings_to_system", true)->values; diff_settings.resize(filament_count + 2); std::vector diff_process_keys; - std::string diff_process_settings = diff_settings[0]; + std::string diff_process_settings = diff_settings[0]; Slic3r::unescape_strings_cstyle(diff_process_settings, diff_process_keys); diff_process_keys.emplace_back("wipe_tower_wall_type"); diff_process_keys.emplace_back("prime_tower_infill_gap"); diff_process_settings = Slic3r::escape_strings_cstyle(diff_process_keys); - diff_settings[0] = diff_process_settings; + diff_settings[0] = diff_process_settings; for (int index = 0; index < filament_count; index++) { std::vector diff_filament_keys; - std::string diff_filament_settings = diff_settings[index + 1]; + std::string diff_filament_settings = diff_settings[index + 1]; Slic3r::unescape_strings_cstyle(diff_filament_settings, diff_filament_keys); diff_filament_keys.emplace_back("filament_adhesiveness_category"); diff_filament_settings = Slic3r::escape_strings_cstyle(diff_filament_keys); @@ -7396,38 +8998,51 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (validated == VALIDATE_PRESETS_MODIFIED_GCODES) { std::string warning_message; warning_message += "\n"; - for (std::set::iterator it=modified_gcodes.begin(); it!=modified_gcodes.end(); ++it) + for (std::set::iterator it = modified_gcodes.begin(); it != modified_gcodes.end(); ++it) warning_message += "-" + *it + "\n"; warning_message += "\n"; - //show_info(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), _L("Modified G-code")); - MessageDialog dlg(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), _L("Modified G-code")); + // show_info(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + + // warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the + // machine!"), _L("Modified G-code")); + MessageDialog + dlg(q, + _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), + _L("Modified G-code")); dlg.show_dsa_button(); - auto res = dlg.ShowModal(); + auto res = dlg.ShowModal(); if (dlg.get_checkbox_state()) wxGetApp().app_config->set("no_warn_when_modified_gcodes", "true"); - } - else if ((validated == VALIDATE_PRESETS_PRINTER_NOT_FOUND) || (validated == VALIDATE_PRESETS_FILAMENTS_NOT_FOUND)) { + } else if ((validated == VALIDATE_PRESETS_PRINTER_NOT_FOUND) || + (validated == VALIDATE_PRESETS_FILAMENTS_NOT_FOUND)) { std::string warning_message; warning_message += "\n"; - for (std::set::iterator it=modified_gcodes.begin(); it!=modified_gcodes.end(); ++it) + for (std::set::iterator it = modified_gcodes.begin(); it != modified_gcodes.end(); ++it) warning_message += "-" + *it + "\n"; warning_message += "\n"; - //show_info(q, _L("The 3MF has the following customized filament or printer presets:") + warning_message + _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), _L("Customized Preset")); - MessageDialog dlg(q, _L("The 3MF has the following customized filament or printer presets:") + from_u8(warning_message)+ _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), _L("Customized Preset")); + // show_info(q, _L("The 3MF has the following customized filament or printer presets:") + warning_message + + // _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), + // _L("Customized Preset")); + MessageDialog dlg(q, + _L("The 3MF has the following customized filament or printer presets:") + + from_u8(warning_message) + + _L("Please confirm that the G-code within these presets is safe to prevent any " + "damage to the machine!"), + _L("Customized Preset")); dlg.show_dsa_button(); - auto res = dlg.ShowModal(); + auto res = dlg.ShowModal(); if (dlg.get_checkbox_state()) wxGetApp().app_config->set("no_warn_when_modified_gcodes", "true"); } } - //always load config + // always load config { // BBS: save the wipe tower pos in file here, will be used later ConfigOptionFloats* wipe_tower_x_opt = config.opt("wipe_tower_x"); ConfigOptionFloats* wipe_tower_y_opt = config.opt("wipe_tower_y"); - std::optionalfile_wipe_tower_x; - std::optionalfile_wipe_tower_y; + std::optional file_wipe_tower_x; + std::optional file_wipe_tower_y; if (wipe_tower_x_opt) file_wipe_tower_x = *wipe_tower_x_opt; if (wipe_tower_y_opt) @@ -7437,9 +9052,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS: notify the user about published settings that could not be applied. if (!published_config.skipped_keys.empty()) { - NotificationManager *notify_manager = q->get_notification_manager(); - std::string message = _u8L("Some published settings could not be applied:"); - for (const std::string &key : published_config.skipped_keys) + NotificationManager* notify_manager = q->get_notification_manager(); + std::string message = _u8L("Some published settings could not be applied:"); + for (const std::string& key : published_config.skipped_keys) message += "\n-" + key; notify_manager->bbl_show_3mf_warn_notification(message); } @@ -7447,16 +9062,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS: notify the user about slot materials that were replaced while // loading a published project (type mismatch / no same-type match). if (!published_config.material_replacements.empty()) { - NotificationManager *notify_manager = q->get_notification_manager(); + NotificationManager* notify_manager = q->get_notification_manager(); std::string message = _u8L("Some filament slots were changed to match the published materials:"); - for (const std::string &replacement : published_config.material_replacements) + for (const std::string& replacement : published_config.material_replacements) message += "\n-" + replacement; notify_manager->bbl_show_3mf_warn_notification(message); } ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type"); if (bed_type_opt != nullptr) { - BedType bed_type = (BedType)bed_type_opt->getInt(); + BedType bed_type = (BedType) bed_type_opt->getInt(); // update app config for bed type bool is_bbl_preset = preset_bundle->is_bbl_vendor(); if (is_bbl_preset) { @@ -7504,9 +9119,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ // // show notification about temporarily installed presets // if (!names.empty()) { - // std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active instance of PrusaSlicer", - // "The presets below were temporarily installed on the active instance of PrusaSlicer", - // names.size())) + ":"; + // std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active + // instance of PrusaSlicer", + // "The presets below were temporarily installed on the active + // instance of PrusaSlicer", names.size())) + ":"; // for (std::string& name : names) // notif_text += "\n - " + name; // notification_manager->push_notification(NotificationType::CustomNotification, @@ -7516,11 +9132,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS // if (printer_technology == ptFFF) - // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, &preset_bundle->project_config); + // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, + // &preset_bundle->project_config); - // For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print Host upload" - // BBS: add preset combo box re-active logic - // currently found only needs re-active here + // For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print + // Host upload" BBS: add preset combo box re-active logic currently found only needs re-active here wxGetApp().load_current_presets(false, false); // Update filament colors for the MM-printer profile in the full config // to avoid black (default) colors for Extruders in the ObjectList, @@ -7531,8 +9147,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicConfig& proj_cfg = preset_bundle->project_config; // do some post process after loading config { - //BBS: rewrite wipe tower pos stored in 3mf file , the code above should be seriously reconsidered - ConfigOptionFloats* wipe_tower_x = proj_cfg.opt("wipe_tower_x"); + // BBS: rewrite wipe tower pos stored in 3mf file , the code above should be seriously reconsidered + ConfigOptionFloats* wipe_tower_x = proj_cfg.opt("wipe_tower_x"); ConfigOptionFloats* wipe_tower_y = proj_cfg.opt("wipe_tower_y"); if (file_wipe_tower_x) *wipe_tower_x = *file_wipe_tower_x; @@ -7560,19 +9176,22 @@ std::vector Plater::priv::load_files(const std::vector& input_ } // Sync filament multi colour - ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", true); + ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", + true); if (filament_multi_color->size() != filament_count) { filament_multi_color->values.resize(filament_count); } // If there is no multi-color data or color is not match, use single color as default value for (size_t i = 0; i < filament_count; i++) { std::vector colors = Slic3r::split_string(filament_multi_color->values[i], ' '); - if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) { + if (i >= filament_multi_color->values.size() || colors.empty() || + colors[0] != filament_color->values[i]) { filament_multi_color->values[i] = filament_color->values[i]; } } // Sync filament colour type - ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", true); + ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", + true); if (filament_color_type && filament_color_type->size() != filament_count) { filament_color_type->values.resize(filament_count); @@ -7602,56 +9221,71 @@ std::vector Plater::priv::load_files(const std::vector& input_ } // The loaded project supplies nozzle_volume_type; refresh the sidebar // nozzle-count badges against it. - if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")) { + if (auto* nozzle_volumes = wxGetApp().preset_bundle->project_config.option( + "nozzle_volume_type")) { const int extruder_count = wxGetApp().preset_bundle->get_printer_extruder_count(); - for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volumes->values.size(); ++extruder_id) - updateNozzleCountDisplay(wxGetApp().preset_bundle, extruder_id, NozzleVolumeType(nozzle_volumes->values[extruder_id])); + for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volumes->values.size(); + ++extruder_id) + updateNozzleCountDisplay(wxGetApp().preset_bundle, extruder_id, + NozzleVolumeType(nozzle_volumes->values[extruder_id])); } } } - if (!silence) wxGetApp().app_config->update_config_dir(path.parent_path().string()); + if (!silence) + wxGetApp().app_config->update_config_dir(path.parent_path().string()); } } else { // BBS: add plate data related logic PlateDataPtrs plate_data; // BBS: project embedded settings - std::vector project_presets; - bool is_xxx; - Semver file_version; + std::vector project_presets; + bool is_xxx; + Semver file_version; - //ObjImportColorFn obj_color_fun=nullptr; - auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { - - if (!boost::iends_with(path.string(), ".obj")) { return; } + // ObjImportColorFn obj_color_fun=nullptr; + auto obj_color_fun = [this, &path](ObjDialogInOut& in_out) { + if (!boost::iends_with(path.string(), ".obj")) { + return; + } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } }; - if (boost::iends_with(path.string(), ".stp") || - boost::iends_with(path.string(), ".step")) { - double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_deflection")); - if (linear <= 0) linear = 0.003; - double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_deflection")); - if (angle <= 0) angle = 0.5; - bool split_compound = wxGetApp().app_config->get_bool("is_split_compound"); - model = Slic3r::Model:: read_from_step(path.string(), strategy, - [this, &dlg, real_filename, &progress_percent, &file_percent, step_percent, INPUT_FILES_RATIO, total_files, i](int load_stage, int current, int total, bool &cancel) - { - bool cont = true; - float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * ((float)step_percent[load_stage] + (float)current * (float)(step_percent[load_stage + 1] - step_percent[load_stage]) / (float)total) / (float)total_files; - BOOST_LOG_TRIVIAL(trace) << "load_step_file: percent(float)=" << percent_float << ", stage = " << load_stage << ", curr = " << current << ", total = " << total; - progress_percent = (int)percent_float; - wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); - cont = dlg.Update(progress_percent, msg); - cancel = !cont; + if (boost::iends_with(path.string(), ".stp") || boost::iends_with(path.string(), ".step")) { + double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_deflection")); + if (linear <= 0) + linear = 0.003; + double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_deflection")); + if (angle <= 0) + angle = 0.5; + bool split_compound = wxGetApp().app_config->get_bool("is_split_compound"); + model = Slic3r::Model::read_from_step( + path.string(), strategy, + [this, &dlg, real_filename, &progress_percent, &file_percent, step_percent, INPUT_FILES_RATIO, total_files, + i](int load_stage, int current, int total, bool& cancel) { + bool cont = true; + float percent_float = (100.0f * (float) i / (float) total_files) + + INPUT_FILES_RATIO * + ((float) step_percent[load_stage] + + (float) current * (float) (step_percent[load_stage + 1] - step_percent[load_stage]) / + (float) total) / + (float) total_files; + BOOST_LOG_TRIVIAL(trace) << "load_step_file: percent(float)=" << percent_float << ", stage = " << load_stage + << ", curr = " << current << ", total = " << total; + progress_percent = (int) percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; }, [](int isUtf8StepFile) { if (!isUtf8StepFile) { const auto no_warn = wxGetApp().app_config->get_bool("step_not_utf8_no_warn"); if (!no_warn) { - MessageDialog dlg(nullptr, _L("Component name(s) inside step file not in UTF8 format!") + "\n\n" + _L("Because of unsupported text encoding, garbage characters may appear!"), + MessageDialog dlg(nullptr, + _L("Component name(s) inside step file not in UTF8 format!") + "\n\n" + + _L("Because of unsupported text encoding, garbage characters may appear!"), wxString(SLIC3R_APP_FULL_NAME " - ") + _L("Attention!"), wxOK | wxICON_INFORMATION); dlg.show_dsa_button(_L("Remember my choice.")); dlg.ShowModal(); @@ -7661,7 +9295,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } }, - [this, &path, &is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, bool& is_split)-> int { + [this, &path, &is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, + double& angle_value, bool& is_split) -> int { if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { StepMeshDialog mesh_dlg(nullptr, file, linear, angle); if (mesh_dlg.ShowModal() == wxID_OK) { @@ -7670,39 +9305,43 @@ std::vector Plater::priv::load_files(const std::vector& input_ is_split = mesh_dlg.get_split_compound_value(); return 1; } - }else { + } else { linear_value = linear; - angle_value = angle; - is_split = split_compound; + angle_value = angle; + is_split = split_compound; return 1; } is_user_cancel = true; return -1; - }, linear, angle, split_compound); - }else { - model = Slic3r::Model:: read_from_file( - path.string(), nullptr, nullptr, strategy, &plate_data, &project_presets, &is_xxx, &file_version, nullptr, - [this, &dlg, real_filename, &progress_percent, &file_percent, INPUT_FILES_RATIO, total_files, i, &designer_model_id, &designer_country_code](int current, int total, bool &cancel, std::string &mode_id, std::string &code) - { - designer_model_id = mode_id; + }, + linear, angle, split_compound); + } else { + model = Slic3r::Model::read_from_file( + path.string(), nullptr, nullptr, strategy, &plate_data, &project_presets, &is_xxx, &file_version, nullptr, + [this, &dlg, real_filename, &progress_percent, &file_percent, INPUT_FILES_RATIO, total_files, i, &designer_model_id, + &designer_country_code](int current, int total, bool& cancel, std::string& mode_id, std::string& code) { + designer_model_id = mode_id; designer_country_code = code; - bool cont = true; - float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * 100.0f * ((float)current / (float)total) / (float)total_files; - BOOST_LOG_TRIVIAL(trace) << "load_stl_file: percent(float)=" << percent_float << ", curr = " << current << ", total = " << total; - progress_percent = (int)percent_float; - wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); - cont = dlg.Update(progress_percent, msg); - cancel = !cont; - }, - nullptr, 0, obj_color_fun); + bool cont = true; + float percent_float = (100.0f * (float) i / (float) total_files) + + INPUT_FILES_RATIO * 100.0f * ((float) current / (float) total) / (float) total_files; + BOOST_LOG_TRIVIAL(trace) + << "load_stl_file: percent(float)=" << percent_float << ", curr = " << current << ", total = " << total; + progress_percent = (int) percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; + }, + nullptr, 0, obj_color_fun); } if (designer_model_id.empty() && boost::algorithm::iends_with(path.string(), ".stl")) { read_binary_stl(path.string(), designer_model_id, designer_country_code); } - if (type_any_amf && is_xxx) imperial_units = true; + if (type_any_amf && is_xxx) + imperial_units = true; for (auto obj : model.objects) { if (obj->name.empty()) { @@ -7723,26 +9362,31 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (project_presets.size() > 0) { // load project embedded presets PresetsConfigSubstitutions preset_substitutions; - PresetBundle & preset_bundle = *wxGetApp().preset_bundle; - preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); - if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, + ForwardCompatibilitySubstitutionRule::Enable); + if (!preset_substitutions.empty()) + show_substitutions_info(preset_substitutions); - for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } + for (unsigned int i = 0; i < project_presets.size(); i++) { + delete project_presets[i]; + } project_presets.clear(); } } - } catch (const ConfigurationError &e) { - std::string message = GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), filename.string()) + "\n\n" + e.what(); + } catch (const ConfigurationError& e) { + std::string message = GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), filename.string()) + + "\n\n" + e.what(); GUI::show_error(q, message); continue; - } catch (const std::exception &e) { + } catch (const std::exception& e) { if (!is_user_cancel) GUI::show_error(q, e.what()); continue; } - progress_percent = 100.0f * (float)i / (float)total_files + INIT_MODEL_RATIO * 100.0f / (float)total_files; - dlg_cont = dlg.Update(progress_percent); + progress_percent = 100.0f * (float) i / (float) total_files + INIT_MODEL_RATIO * 100.0f / (float) total_files; + dlg_cont = dlg.Update(progress_percent); if (!dlg_cont) { q->skip_thumbnail_invalid = false; return empty_result; @@ -7750,13 +9394,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_model) { // The model should now be initialized - auto convert_from_imperial_units = [](Model &model, bool only_small_volumes) { model.convert_from_imperial_units(only_small_volumes); }; + auto convert_from_imperial_units = [](Model& model, bool only_small_volumes) { + model.convert_from_imperial_units(only_small_volumes); + }; // BBS: add load_old_project logic if ((!is_project_file) && (!load_old_project)) { // if (!is_project_file) { if (int deleted_objects = model.removed_objects_with_zero_volume(); deleted_objects > 0) { - MessageDialog(q, _L("Objects with zero volume removed"), _L("The volume of the object is zero"), wxICON_INFORMATION | wxOK).ShowModal(); + MessageDialog(q, _L("Objects with zero volume removed"), _L("The volume of the object is zero"), + wxICON_INFORMATION | wxOK) + .ShowModal(); } if (imperial_units) // Convert even if the object is big. @@ -7764,19 +9412,23 @@ std::vector Plater::priv::load_files(const std::vector& input_ else if (model.looks_like_saved_in_meters()) { // BBS do not handle look like in meters MessageDialog dlg(q, - format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\n Do you want to scale to millimeters\?"), + format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\n Do you want " + "to scale to millimeters\?"), from_path(filename)), _L("Object too small"), wxICON_QUESTION | wxYES_NO); - int answer = dlg.ShowModal(); - if (answer == wxID_YES) model.convert_from_meters(true); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) + model.convert_from_meters(true); } else if (model.looks_like_imperial_units()) { // BBS do not handle look like in meters MessageDialog dlg(q, - format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\n Do you want to scale to millimeters\?"), + format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\n Do you want " + "to scale to millimeters\?"), from_path(filename)), _L("Object too small"), wxICON_QUESTION | wxYES_NO); - int answer = dlg.ShowModal(); - if (answer == wxID_YES) convert_from_imperial_units(model, true); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) + convert_from_imperial_units(model, true); } // else if (model.looks_like_imperial_units()) { // BBS do not handle look like in imperial @@ -7790,7 +9442,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ // "The dimensions of the object from file %s seem to be defined in inches.\n" // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of the object?", // "The dimensions of some objects from file %s seem to be defined in inches.\n" - // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) + // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", + // model.objects.size()), from_path(filename)) // + "\n", _L("The object is too small"), wxICON_QUESTION | wxYES_NO); // dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded.")); // int answer = dlg.ShowModal(); @@ -7802,9 +9455,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ // convert_model_if(model, answer_convert_from_imperial_units == wxID_YES); } - if (!is_project_file && model.looks_like_multipart_object()) { - MessageDialog msg_dlg(q, _L("This file contains several objects positioned at multiple heights.\nInstead of considering them as multiple objects, should \nthe file be loaded as a single object with multiple parts\?") + "\n", - _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); + if (!is_project_file && model.looks_like_multipart_object()) { + MessageDialog msg_dlg(q, + _L("This file contains several objects positioned at multiple heights.\nInstead of considering them " + "as multiple objects, should \nthe file be loaded as a single object with multiple parts\?") + + "\n", + _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); if (msg_dlg.ShowModal() == wxID_YES) { model.convert_multipart_object(filaments_cnt); } @@ -7821,15 +9477,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ // return obj_idxs; //} - progress_percent = 100.0f * (float)i / (float)total_files + CENTER_AROUND_ORIGIN_RATIO * 100.0f / (float)total_files; - dlg_cont = dlg.Update(progress_percent); + progress_percent = 100.0f * (float) i / (float) total_files + CENTER_AROUND_ORIGIN_RATIO * 100.0f / (float) total_files; + dlg_cont = dlg.Update(progress_percent); if (!dlg_cont) { q->skip_thumbnail_invalid = false; return empty_result; } int model_idx = 0; - for (ModelObject *model_object : model.objects) { + for (ModelObject* model_object : model.objects) { if (!type_3mf && !type_any_amf) model_object->center_around_origin(false); @@ -7849,8 +9505,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ tolal_model_count += model_idx; - progress_percent = 100.0f * (float)i / (float)total_files + LOAD_MODEL_RATIO * 100.0f / (float)total_files; - dlg_cont = dlg.Update(progress_percent); + progress_percent = 100.0f * (float) i / (float) total_files + LOAD_MODEL_RATIO * 100.0f / (float) total_files; + dlg_cont = dlg.Update(progress_percent); if (!dlg_cont) { q->skip_thumbnail_invalid = false; return empty_result; @@ -7867,7 +9523,37 @@ std::vector Plater::priv::load_files(const std::vector& input_ q->model().load_from(model); load_auxiliary_files(); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", before load_model_objects, count %1%")%model.objects.size(); + // Texture-to-color: a mesh that arrived with UVs and a decoded texture gets its + // faces clustered into printable colours and matched against the filament list, + // before the objects are handed to the plater. Inert for every other model. + if (model.texture_mesh && has_importable_texture(*model.texture_mesh)) { + TextureImportResult texture_import_result; + auto cancel_cb = [&dlg, &dlg_cont]() { return !dlg_cont || dlg.WasCancelled(); }; + auto progress_cb = [&dlg, &dlg_cont, &progress_percent](int percent) { + progress_percent = std::clamp(percent, 0, 100); + dlg_cont = dlg.Update(progress_percent, _L("Matching textures to filaments")); + return dlg_cont; + }; + if (!run_textured_mesh_import_dialog(model, texture_import_result, cancel_cb, progress_cb)) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + if (texture_import_result.fallback_to_geometry_only && !texture_import_result.fallback_warning.empty()) { + MessageDialog(q, texture_import_result.fallback_warning, _L("Texture Import Warning"), wxOK | wxICON_WARNING).ShowModal(); + } + if (!texture_import_result.painted.face_colors.empty()) { + std::vector texture_object_idxs(model.objects.size()); + std::iota(texture_object_idxs.begin(), texture_object_idxs.end(), 0); + auto apply_progress_cb = [&dlg](int percent, const wxString& msg) { + dlg.Update(std::clamp(percent, 0, 100), msg); + return true; + }; + apply_textured_mesh_import_result(model, texture_object_idxs, texture_import_result, apply_progress_cb, false); + } + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << boost::format(", before load_model_objects, count %1%") % model.objects.size(); auto loaded_idxs = load_model_objects(model.objects, is_project_file); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); @@ -7880,10 +9566,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else { // This must be an .stl or .obj file, which may contain a maximum of one volume. - for (const ModelObject *model_object : model.objects) { + for (const ModelObject* model_object : model.objects) { new_model->add_object(*model_object); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":" << __LINE__ << boost::format(", added object %1%")%model_object->name; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":" << __LINE__ << boost::format(", added object %1%") % model_object->name; wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); dlg_cont = dlg.Update(progress_percent, msg); if (!dlg_cont) { @@ -7895,18 +9581,18 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (new_model != nullptr && new_model->objects.size() > 1) { - //BBS do not popup this dialog + // BBS do not popup this dialog bool new_model_auto_drop = true; int single_object_answer = false; if (ask_multi) { RichMessageDialog dlg(q, _L("Load these files as a single object with multiple parts?\n"), - _L("An object with multiple parts was detected"), wxICON_QUESTION | wxYES_NO); + _L("An object with multiple parts was detected"), wxICON_QUESTION | wxYES_NO); dlg.ShowCheckBox(_L("Auto-Drop"), true); single_object_answer = dlg.ShowModal(); - if (dlg.IsCheckBoxChecked() == false) + if (dlg.IsCheckBoxChecked() == false) new_model_auto_drop = false; // convert to multipart and split after load_model_objects @@ -7918,8 +9604,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ // TODO // DONE always convert to multipart, split afterwards to retain relative position // DONE if !auto_drop move all objects over the z-position 0, so that none are clipped by the bed. - // DONE retain auto_drop (and printable) state when assembling or splitting objects. - // DONE when manually split to object ask users if looks_like_multipart and none have auto_drob disabled if they want to disable auto_drop for all resulting objects. + // DONE retain auto_drop (and printable) state when assembling or splitting objects. + // DONE when manually split to object ask users if looks_like_multipart and none have auto_drob disabled if they want to disable + // auto_drop for all resulting objects. // - add icon in object list, similar to fuzzy painting, etc. auto loaded_idxs = load_model_objects(new_model->objects, false, false, new_model_auto_drop); @@ -7931,41 +9618,48 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (load_config) { - DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (dev) { - MachineObject *obj = dev->get_selected_machine(); + MachineObject* obj = dev->get_selected_machine(); if (obj && obj->is_info_ready()) { if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) { - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - Preset &printer_preset = preset_bundle->printers.get_selected_preset(); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + Preset& printer_preset = preset_bundle->printers.get_selected_preset(); - double preset_nozzle_diameter = 0.4; - const ConfigOption *opt = printer_preset.config.option("nozzle_diameter"); - if (opt) preset_nozzle_diameter = static_cast(opt)->values[0]; + double preset_nozzle_diameter = 0.4; + const ConfigOption* opt = printer_preset.config.option("nozzle_diameter"); + if (opt) + preset_nozzle_diameter = static_cast(opt)->values[0]; std::string machine_type = obj->printer_type; - if (obj->is_support_upgrade_kit && obj->installed_upgrade_kit) machine_type = "C12"; + if (obj->is_support_upgrade_kit && obj->installed_upgrade_kit) + machine_type = "C12"; bool nozzle_mismatch = !obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, (float) preset_nozzle_diameter); if (printer_preset.get_current_printer_type(preset_bundle) != machine_type || nozzle_mismatch) { - Preset *machine_preset = get_printer_preset(obj); + Preset* machine_preset = get_printer_preset(obj); if (machine_preset != nullptr) { std::string printer_model = machine_preset->config.option("printer_model")->value; - bool sync_printer_info = false; + bool sync_printer_info = false; if (!wxGetApp().app_config->has("sync_after_load_file_show_flag")) { - wxString tips = from_u8((boost::format(_u8L("Connected printer is %s. It must match the project preset for printing.\n")) % printer_model).str()); + wxString tips = from_u8( + (boost::format(_u8L("Connected printer is %s. It must match the project preset for printing.\n")) % + printer_model) + .str()); tips += _L("Do you want to sync the printer information and automatically switch the preset?"); TipsDialog dlg(wxGetApp().mainframe, _L("Tips"), tips, "sync_after_load_file_show_flag", wxYES_NO); - if (dlg.ShowModal() == wxID_YES) { sync_printer_info = true; } - } - else { + if (dlg.ShowModal() == wxID_YES) { + sync_printer_info = true; + } + } else { sync_printer_info = wxGetApp().app_config->get("sync_after_load_file_show_flag") == "true"; } if (sync_printer_info) { - Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); + Tab* printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); printer_tab->select_preset(machine_preset->name); - if (obj->is_multi_extruders()) GUI::wxGetApp().sidebar().sync_extruder_list(); + if (obj->is_multi_extruders()) + GUI::wxGetApp().sidebar().sync_extruder_list(); } } } @@ -7974,14 +9668,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - if (new_model) delete new_model; + if (new_model) + delete new_model; - //BBS: translate old 3mf to correct positions + // BBS: translate old 3mf to correct positions if (translate_old) { - //translate the objects + // translate the objects int plate_count = partplate_list.get_plate_count(); - for (int index = 1; index < plate_count; index ++) { - PartPlate* cur_plate = (PartPlate *)partplate_list.get_plate(index); + for (int index = 1; index < plate_count; index++) { + PartPlate* cur_plate = (PartPlate*) partplate_list.get_plate(index); Vec3d cur_origin = cur_plate->get_origin(); Vec3d new_origin = partplate_list.compute_origin_using_new_size(index, current_width, current_depth); @@ -7993,50 +9688,46 @@ std::vector Plater::priv::load_files(const std::vector& input_ partplate_list.register_raycasters_for_picking(*view3D->get_canvas3d()); } - //BBS: add gcode loading logic in the end - q->m_exported_file = false; + // BBS: add gcode loading logic in the end + q->m_exported_file = false; q->skip_thumbnail_invalid = false; if (load_model && load_config) { if (model.objects.empty()) { partplate_list.load_gcode_files(); - PartPlate * first_plate = nullptr, *cur_plate = nullptr; + PartPlate *first_plate = nullptr, *cur_plate = nullptr; int plate_cnt = partplate_list.get_plate_count(); int index = 0, first_plate_index = 0; q->m_valid_plates_count = 0; - for (index = 0; index < plate_cnt; index ++) - { + for (index = 0; index < plate_cnt; index++) { cur_plate = partplate_list.get_plate(index); if (!first_plate && cur_plate->is_slice_result_valid()) { - first_plate = cur_plate; + first_plate = cur_plate; first_plate_index = index; } if (cur_plate->is_slice_result_valid()) - q->m_valid_plates_count ++; + q->m_valid_plates_count++; } - if (first_plate&&first_plate->is_slice_result_valid()) { + if (first_plate && first_plate->is_slice_result_valid()) { q->m_exported_file = true; - //select plate 0 as default + // select plate 0 as default q->select_plate(first_plate_index); - //set to 3d tab + // set to 3d tab q->select_view_3D("Preview"); wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); - } - else { - //set to 3d tab + } else { + // set to 3d tab q->select_view_3D("3D"); - //select plate 0 as default + // select plate 0 as default q->select_plate(0); } - } - else { - //set to 3d tab + } else { + // set to 3d tab q->select_view_3D("3D"); - //select plate 0 as default + // select plate 0 as default q->select_plate(0); } - } - else { - //always set to 3D after loading files + } else { + // always set to 3D after loading files q->select_view_3D("3D"); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } @@ -8048,7 +9739,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ view3D->reload_scene(true); view3D->set_as_dirty(); } - if (!silence) wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); + if (!silence) + wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); // XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames... } @@ -8061,7 +9753,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ Selection& selection = view3D->get_canvas3d()->get_selection(); selection.clear(); for (size_t idx : obj_idxs) { - selection.add_object((unsigned int)idx, false); + selection.add_object((unsigned int) idx, false); } } // BBS: update object list selection @@ -8072,25 +9764,25 @@ std::vector Plater::priv::load_files(const std::vector& input_ view3D->get_canvas3d()->update_gizmos_on_off_state(); } - GLGizmoSimplify::add_simplify_suggestion_notification( - obj_idxs, model.objects, *notification_manager); + GLGizmoSimplify::add_simplify_suggestion_notification(obj_idxs, model.objects, *notification_manager); - //set designer_model_id - q->model().stl_design_id = designer_model_id; + // set designer_model_id + q->model().stl_design_id = designer_model_id; q->model().stl_design_country = designer_country_code; - //if (!designer_model_id.empty() && q->model().stl_design_id.empty() && !designer_country_code.empty()) { - // q->model().stl_design_id = designer_model_id; - // q->model().stl_design_country = designer_country_code; - //} - //else { - // q->model().stl_design_id = ""; - // q->model().stl_design_country = ""; - //} + // if (!designer_model_id.empty() && q->model().stl_design_id.empty() && !designer_country_code.empty()) { + // q->model().stl_design_id = designer_model_id; + // q->model().stl_design_country = designer_country_code; + // } + // else { + // q->model().stl_design_id = ""; + // q->model().stl_design_country = ""; + // } if (tolal_model_count <= 0 && !q->m_exported_file) { dlg.Hide(); if (!is_user_cancel) { - MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING); + MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), + wxYES | wxICON_WARNING); if (msg.ShowModal() == wxID_YES) {} } } @@ -8099,9 +9791,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ return obj_idxs; } - #define AUTOPLACEMENT_ON_LOAD +#define AUTOPLACEMENT_ON_LOAD -std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z, bool split_object, bool auto_drop) +std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, + bool allow_negative_z, + bool split_object, + bool auto_drop) { const Vec3d bed_size = Slic3r::to_3d(this->bed.build_volume().bounding_volume2d().size(), 1.0) - 2.0 * Vec3d::Ones(); @@ -8115,8 +9810,8 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #ifdef AUTOPLACEMENT_ON_LOAD ModelInstancePtrs new_instances; #endif /* AUTOPLACEMENT_ON_LOAD */ - for (ModelObject *model_object : model_objects) { - auto *object = model.add_object(*model_object); + for (ModelObject* model_object : model_objects) { + auto* object = model.add_object(*model_object); object->sort_volumes(true); std::string object_name = object->name.empty() ? fs::path(object->input_file).filename().string() : object->name; obj_idxs.push_back(obj_count++); @@ -8125,29 +9820,30 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #ifdef AUTOPLACEMENT_ON_LOAD object->center_around_origin(); new_instances.emplace_back(object->add_instance()); -#else /* AUTOPLACEMENT_ON_LOAD */ +#else /* AUTOPLACEMENT_ON_LOAD */ // if object has no defined position(s) we need to rearrange everything after loading // need_arrange = true; - // add a default instance and center object around origin - object->center_around_origin(); // also aligns object to Z = 0 + // add a default instance and center object around origin + object->center_around_origin(); // also aligns object to Z = 0 ModelInstance* instance = object->add_instance(); - //BBS calc transformation + // BBS calc transformation Geometry::Transformation t = instance->get_transformation(); instance->set_offset(Slic3r::to_3d(this->bed.build_volume().bed_center(), -object->origin_translation(2))); #endif /* AUTOPLACEMENT_ON_LOAD */ } - - //BBS: when the object is too large, let the user choose whether to scale it down + + // BBS: when the object is too large, let the user choose whether to scale it down for (size_t i = 0; i < object->instances.size(); ++i) { ModelInstance* instance = object->instances[i]; - const Vec3d size = object->instance_bounding_box(i).size(); - const Vec3d ratio = size.cwiseQuotient(bed_size); - const double max_ratio = std::max(ratio(0), ratio(1)); + const Vec3d size = object->instance_bounding_box(i).size(); + const Vec3d ratio = size.cwiseQuotient(bed_size); + const double max_ratio = std::max(ratio(0), ratio(1)); if (max_ratio > 10000) { - MessageDialog dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), _L("Object too large"), - wxICON_QUESTION | wxYES); - int answer = dlg.ShowModal(); + MessageDialog + dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), + _L("Object too large"), wxICON_QUESTION | wxYES); + int answer = dlg.ShowModal(); // the size of the object is too big -> this could lead to overflow when moving to clipper coordinates, // so scale down the mesh object->scale_mesh_after_creation(1. / max_ratio); @@ -8155,11 +9851,11 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode object->center_around_origin(); scaled_down = true; break; - } - else if (max_ratio > 10) { - MessageDialog dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), _L("Object too large"), - wxICON_QUESTION | wxYES_NO); - int answer = dlg.ShowModal(); + } else if (max_ratio > 10) { + MessageDialog + dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), + _L("Object too large"), wxICON_QUESTION | wxYES_NO); + int answer = dlg.ShowModal(); if (answer == wxID_YES) { instance->set_scaling_factor(instance->get_scaling_factor() / max_ratio); scaled_down = true; @@ -8169,28 +9865,27 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode if (!auto_drop) { for (size_t i = 0; i < object->instances.size(); ++i) { - ModelInstance* instance = object->instances[i]; - instance->auto_drop = auto_drop; + ModelInstance* instance = object->instances[i]; + instance->auto_drop = auto_drop; } // if under the bed, move over the bed double dist_to_bed = std::min(object->min_z(), double(0)); object->translate_instances(Vec3d(0, 0, -dist_to_bed)); - } - else { + } else { object->ensure_on_bed(allow_negative_z); } if (!split_object) { - //BBS initial assemble transformation + // BBS initial assemble transformation for (ModelObject* model_object : model.objects) { - //BBS initialize assemble transformation + // BBS initialize assemble transformation for (int i = 0; i < model_object->instances.size(); i++) { if (!model_object->instances[i]->is_assemble_initialized()) { model_object->instances[i]->set_assemble_transformation(model_object->instances[i]->get_transformation()); } } - } + } } } @@ -8216,7 +9911,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #else // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { - auto offset = instance->get_offset(); + auto offset = instance->get_offset(); auto start_point = this->bed.build_volume().bounding_volume2d().center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; @@ -8232,23 +9927,22 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #endif /* AUTOPLACEMENT_ON_LOAD */ - //BBS: remove the auto scaled_down logic when load models - //if (scaled_down) { - // GUI::show_info(q, - // _L("Your object appears to be too large, so it was automatically scaled down to fit your print bed."), - // _L("Object too large?")); - //} + // BBS: remove the auto scaled_down logic when load models + // if (scaled_down) { + // GUI::show_info(q, + // _L("Your object appears to be too large, so it was automatically scaled down to fit your print bed."), + // _L("Object too large?")); + // } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished auto placement, before add_objects_to_list"); notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo); if (obj_idxs.size() > 1) { - std::vector obj_idxs_1 (obj_idxs.begin(), obj_idxs.end() - 1); + std::vector obj_idxs_1(obj_idxs.begin(), obj_idxs.end() - 1); wxGetApp().obj_list()->add_objects_to_list(obj_idxs_1, false); wxGetApp().obj_list()->add_object_to_list(obj_idxs[obj_idxs.size() - 1]); - } - else + } else wxGetApp().obj_list()->add_objects_to_list(obj_idxs); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", after add_objects_to_list"); @@ -8257,7 +9951,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call for (const size_t idx : obj_idxs) wxGetApp().obj_list()->update_info_items(idx); - + object_list_changed(); this->schedule_background_process(); @@ -8269,7 +9963,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode void Plater::priv::load_auxiliary_files() { std::string auxiliary_path = encode_path(q->model().get_auxiliary_file_temp_path().c_str()); - //wxGetApp().mainframe->m_project->Reload(auxiliary_path); + // wxGetApp().mainframe->m_project->Reload(auxiliary_path); } fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) @@ -8278,7 +9972,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) this->update_print_volume_state(); const Selection& selection = get_selection(); - int obj_idx = selection.get_object_idx(); + int obj_idx = selection.get_object_idx(); fs::path output_file; if (file_type == FT_3MF) @@ -8287,22 +9981,20 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) else if (file_type == FT_STL) { if (obj_idx > 0 && obj_idx < this->model.objects.size() && selection.is_single_full_object()) { output_file = this->model.objects[obj_idx]->get_export_filename(); - } - else { + } else { output_file = into_path(get_project_name()); } } - //bbs name the project using the part name + // bbs name the project using the part name if (output_file.empty()) { if (get_project_name() != _L("Untitled")) { output_file = into_path(get_project_name() + ".3mf"); } } - if (output_file.empty()) - { + if (output_file.empty()) { // first try to get the file name from the current selection - if ((0 <= obj_idx) && (obj_idx < (int)this->model.objects.size())) + if ((0 <= obj_idx) && (obj_idx < (int) this->model.objects.size())) output_file = this->model.objects[obj_idx]->get_export_filename(); if (output_file.empty()) @@ -8324,62 +10016,53 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& { wxString wildcard; switch (file_type) { - case FT_STL: - case FT_DRC: - case FT_AMF: - case FT_3MF: - case FT_GCODE: - case FT_OBJ: - wildcard = file_wildcards(file_type); - break; - default: - wildcard = file_wildcards(FT_MODEL); - break; + case FT_STL: + case FT_DRC: + case FT_AMF: + case FT_3MF: + case FT_GCODE: + case FT_OBJ: wildcard = file_wildcards(file_type); break; + default: wildcard = file_wildcards(FT_MODEL); break; } fs::path output_file = get_export_file_path(file_type); wxString dlg_title; switch (file_type) { - case FT_STL: - { - output_file.replace_extension("stl"); - dlg_title = _L("Export STL file:"); - break; - } - case FT_DRC: - { - output_file.replace_extension("drc"); - dlg_title = _L("Export Draco file:"); - break; - } - case FT_AMF: - { - // XXX: Problem on OS X with double extension? - output_file.replace_extension("zip.amf"); - dlg_title = _L("Export AMF file:"); - break; - } - case FT_3MF: - { - output_file.replace_extension("3mf"); - dlg_title = title.empty() ? _L("Save file as") : title; - break; - } - case FT_OBJ: - { - output_file.replace_extension("obj"); - dlg_title = _L("Export OBJ file:"); - break; - } - default: break; + case FT_STL: { + output_file.replace_extension("stl"); + dlg_title = _L("Export STL file:"); + break; + } + case FT_DRC: { + output_file.replace_extension("drc"); + dlg_title = _L("Export Draco file:"); + break; + } + case FT_AMF: { + // XXX: Problem on OS X with double extension? + output_file.replace_extension("zip.amf"); + dlg_title = _L("Export AMF file:"); + break; + } + case FT_3MF: { + output_file.replace_extension("3mf"); + dlg_title = title.empty() ? _L("Save file as") : title; + break; + } + case FT_OBJ: { + output_file.replace_extension("obj"); + dlg_title = _L("Export OBJ file:"); + break; + } + default: break; } std::string out_dir = (boost::filesystem::path(output_file).parent_path()).string(); wxFileDialog dlg(q, dlg_title, - is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), from_path(output_file.filename()), - wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxPD_APP_MODAL); + is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), + from_path(output_file.filename()), wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxPD_APP_MODAL); int result = dlg.ShowModal(); if (result == wxID_CANCEL) @@ -8395,9 +10078,8 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& boost::system::error_code ec; if (boost::filesystem::exists(into_u8(out_path), ec)) { auto result = MessageBox(q->GetHandle(), - wxString::Format(_L("The file %s already exists.\nDo you want to replace it\?"), out_path), - _L("Confirm Save As"), - MB_YESNO | MB_ICONWARNING); + wxString::Format(_L("The file %s already exists.\nDo you want to replace it\?"), out_path), + _L("Confirm Save As"), MB_YESNO | MB_ICONWARNING); if (result != IDYES) return wxEmptyString; } @@ -8408,20 +10090,11 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& return out_path; } -const Selection& Plater::priv::get_selection() const -{ - return view3D->get_canvas3d()->get_selection(); -} +const Selection& Plater::priv::get_selection() const { return view3D->get_canvas3d()->get_selection(); } -Selection& Plater::priv::get_selection() -{ - return view3D->get_canvas3d()->get_selection(); -} +Selection& Plater::priv::get_selection() { return view3D->get_canvas3d()->get_selection(); } -Selection& Plater::priv::get_curr_selection() -{ - return get_current_canvas3D()->get_selection(); -} +Selection& Plater::priv::get_curr_selection() { return get_current_canvas3D()->get_selection(); } int Plater::priv::get_selected_object_idx() const { @@ -8432,9 +10105,9 @@ int Plater::priv::get_selected_object_idx() const int Plater::priv::get_selected_volume_idx() const { auto& selection = get_selection(); - int idx = selection.get_object_idx(); + int idx = selection.get_object_idx(); if ((0 > idx) || (idx > 1000)) - return-1; + return -1; const GLVolume* v = selection.get_first_volume(); if (model.objects[idx]->volumes.size() > 1) return v->volume_idx(); @@ -8461,17 +10134,22 @@ void Plater::priv::object_list_changed() { const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty()); // XXX: is this right? - //const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; + // const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; ObjectFilamentResults object_results; bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; - model_fits = model_fits && object_results.filaments.empty(); + model_fits = model_fits && object_results.filaments.empty(); PartPlate* part_plate = partplate_list.get_curr_plate(); // BBS - //sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances()); - bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": can_slice %1%, model_fits= %2%, export_in_progress %3%, has_printable_instances %4% ")%can_slice %model_fits %export_in_progress %part_plate->has_printable_instances(); + // sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances()); + // A mixed filament with deleted or type-mismatched components cannot be resolved at slicing + // time, so block the slice buttons the same way MainFrame::get_enable_slice_status() does. + bool mixed_broken = sidebar->has_broken_mixed_filament(); + bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances() && !mixed_broken; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": can_slice %1%, model_fits= %2%, export_in_progress %3%, has_printable_instances %4% ") % + can_slice % model_fits % export_in_progress % part_plate->has_printable_instances(); main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); wxGetApp().params_panel()->notify_object_config_changed(); @@ -8498,15 +10176,9 @@ void Plater::priv::select_all() this->sidebar->obj_list()->update_selections(); } -void Plater::priv::deselect_all() -{ - view3D->deselect_all(); -} +void Plater::priv::deselect_all() { view3D->deselect_all(); } -void Plater::priv::exit_gizmo() -{ - view3D->exit_gizmo(); -} +void Plater::priv::exit_gizmo() { view3D->exit_gizmo(); } void Plater::priv::remove(size_t obj_idx) { @@ -8515,7 +10187,7 @@ void Plater::priv::remove(size_t obj_idx) m_worker.cancel_all(); model.delete_object(obj_idx); - //BBS: notify partplate the instance removed + // BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); update(); // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. @@ -8523,15 +10195,15 @@ void Plater::priv::remove(size_t obj_idx) object_list_changed(); } - bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immediately) { // check if object isn't cut // show warning message that "cut consistancy" will not be supported any more - ModelObject *obj = model.objects[obj_idx]; + ModelObject* obj = model.objects[obj_idx]; if (obj->is_cut()) { InfoDialog dialog(q, _L("Delete object which is a part of cut object"), - _L("You are trying to delete an object which is a part of a cut object.\nThis action will break a cut correspondence.\nAfter that, model consistency can\'t be guaranteed."), + _L("You are trying to delete an object which is a part of a cut object.\nThis action will break a cut " + "correspondence.\nAfter that, model consistency can\'t be guaranteed."), false, wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); dialog.SetButtonLabel(wxID_YES, _L("Delete")); if (dialog.ShowModal() == wxID_CANCEL) @@ -8548,10 +10220,10 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); model.delete_object(obj_idx); - //BBS: notify partplate the instance removed + // BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); - //BBS + // BBS if (refresh_immediately) { update(); object_list_changed(); @@ -8577,7 +10249,7 @@ void Plater::priv::delete_all_objects_from_model() // Stop and reset the Print content. background_process.reset(); - //BBS: update partplate + // BBS: update partplate partplate_list.clear(); model.clear_objects(); @@ -8586,7 +10258,7 @@ void Plater::priv::delete_all_objects_from_model() sidebar->obj_list()->delete_all_objects_from_list(); object_list_changed(); - //BBS + // BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); } @@ -8605,16 +10277,16 @@ void Plater::priv::reset(bool apply_presets_change) view3D->get_canvas3d()->reset_all_gizmos(); reset_gcode_toolpaths(); - //BBS: update gcode to current partplate's - //GCodeProcessorResult* current_result = this->background_process.get_current_plate()->get_slice_result(); - //current_result->reset(); - //gcode_result.reset(); + // BBS: update gcode to current partplate's + // GCodeProcessorResult* current_result = this->background_process.get_current_plate()->get_slice_result(); + // current_result->reset(); + // gcode_result.reset(); view3D->get_canvas3d()->reset_sequential_print_clearance(); m_worker.cancel_all(); - //BBS: clear the partplate list's object before object cleared + // BBS: clear the partplate list's object before object cleared partplate_list.reinit(); partplate_list.update_slice_context_to_current_plate(background_process); preview->update_gcode_result(partplate_list.get_current_slice_result()); @@ -8625,7 +10297,7 @@ void Plater::priv::reset(bool apply_presets_change) assemble_view->get_canvas3d()->reset_explosion_ratio(); update(); - //BBS + // BBS if (wxGetApp().is_editor()) { // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar->obj_list()->delete_all_objects_from_list(); @@ -8635,14 +10307,14 @@ void Plater::priv::reset(bool apply_presets_change) project.reset(); wxGetApp().sidebar().printer_combox()->clear_selected_dev_id(); - //BBS: reset all project embedded presets + // BBS: reset all project embedded presets wxGetApp().preset_bundle->reset_project_embedded_presets(); if (apply_presets_change) wxGetApp().apply_keeped_preset_modifications(); else wxGetApp().load_current_presets(false, false); - //BBS + // BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); @@ -8661,28 +10333,19 @@ void Plater::priv::reset(bool apply_presets_change) } } -void Plater::priv::center_selection() -{ - view3D->center_selected(); -} +void Plater::priv::center_selection() { view3D->center_selected(); } -void Plater::priv::drop_selection() -{ - view3D->drop_selected(); -} +void Plater::priv::drop_selection() { view3D->drop_selected(); } -void Plater::priv::mirror(Axis axis) -{ - view3D->mirror_selection(axis); -} +void Plater::priv::mirror(Axis axis) { view3D->mirror_selection(axis); } -void Plater::find_new_position(const ModelInstancePtrs &instances) +void Plater::find_new_position(const ModelInstancePtrs& instances) { arrangement::ArrangePolygons movable, fixed; arrangement::ArrangeParams arr_params = init_arrange_params(this); - for (const ModelObject *mo : p->model.objects) - for (ModelInstance *inst : mo->instances) { + for (const ModelObject* mo : p->model.objects) + for (ModelInstance* inst : mo->instances) { auto it = std::find(instances.begin(), instances.end(), inst); arrangement::ArrangePolygon arrpoly; inst->get_arrange_polygon(&arrpoly); @@ -8690,7 +10353,7 @@ void Plater::find_new_position(const ModelInstancePtrs &instances) if (it == instances.end()) fixed.emplace_back(std::move(arrpoly)); else { - arrpoly.setter = [it](const arrangement::ArrangePolygon &p) { + arrpoly.setter = [it](const arrangement::ArrangePolygon& p) { if (p.is_arranged() && p.bed_idx == 0) { Vec2d t = p.translation.cast(); (*it)->apply_arrange_result(t, p.rotation); @@ -8705,13 +10368,13 @@ void Plater::find_new_position(const ModelInstancePtrs &instances) arrangement::arrange(movable, fixed, this->build_volume().polygon(), arr_params); - for (auto & m : movable) + for (auto& m : movable) m.apply(); } // split selected object into multiple objects by its volumes void Plater::priv::split_object(bool auto_drop /* = true */) -{ +{ int obj_idx = get_selected_object_idx(); priv::split_object(obj_idx, auto_drop); } @@ -8724,7 +10387,7 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) // we clone model object because split_object() adds the split volumes // into the same model object, thus causing duplicates when we call load_model_objects() - Model new_model = model; + Model new_model = model; ModelObject* current_model_object = new_model.objects[obj_idx]; wxBusyCursor wait; @@ -8733,11 +10396,10 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) if (new_objects.size() == 1) // #ysFIXME use notification Slic3r::GUI::warning_catcher(q, _L("The selected object couldn't be split.")); - else - { + else { // BBS no solid parts removed // If we splited object which is contain some parts/modifiers then all non-solid parts (modifiers) were deleted - //if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size()) + // if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size()) // notification_manager->push_notification(NotificationType::CustomNotification, // NotificationManager::NotificationLevel::PrintInfoNotificationLevel, // _u8L("All non-solid parts (modifiers) were deleted")); @@ -8746,15 +10408,15 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) auto is_atleast_one_floating = [new_objects]() { for (ModelObject* new_object : new_objects) { - if (new_object->get_instance_min_z(0) >= SINKING_MIN_Z_THRESHOLD) + if (new_object->get_instance_min_z(0) >= SINKING_MIN_Z_THRESHOLD) return true; } return false; }; bool split_auto_drop = auto_drop; if (current_model_object->instances[0]->auto_drop && is_atleast_one_floating()) { - MessageDialog dlg(q, _L("Disable Auto-Drop to preserve Z positioning?\n"), - _L("Object with floating parts was detected"), wxICON_QUESTION | wxYES_NO); + MessageDialog dlg(q, _L("Disable Auto-Drop to preserve Z positioning?\n"), _L("Object with floating parts was detected"), + wxICON_QUESTION | wxYES_NO); if (dlg.ShowModal() == wxID_YES) split_auto_drop = false; @@ -8764,23 +10426,19 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) // load all model objects at once, otherwise the plate would be rearranged after each one // causing original positions not to be kept - //BBS: set split_object to true to avoid re-compute assemble matrix + // BBS: set split_object to true to avoid re-compute assemble matrix std::vector idxs = load_model_objects(new_objects, false, true, split_auto_drop); wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(idxs); // select newly added objects - for (size_t idx : idxs) - { - get_selection().add_object((unsigned int)idx, false); + for (size_t idx : idxs) { + get_selection().add_object((unsigned int) idx, false); } } } -void Plater::priv::split_volume() -{ - wxGetApp().obj_list()->split(); -} +void Plater::priv::split_volume() { wxGetApp().obj_list()->split(); } void Plater::priv::scale_selection_to_fit_print_volume() { @@ -8893,11 +10551,11 @@ int Plater::priv::auto_slice_delay_seconds() const std::vector> Plater::priv::get_extruder_filament_info() { std::vector> filament_infos; - DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return filament_infos; - MachineObject *obj_ = dev->get_selected_machine(); + MachineObject* obj_ = dev->get_selected_machine(); if (obj_ == nullptr) return filament_infos; @@ -8910,90 +10568,97 @@ std::vector> Plater::priv::get_extruder_filament void Plater::priv::update_print_volume_state() { - //BBS: use the plate's bounding box instead of the bed's + // BBS: use the plate's bounding box instead of the bed's PartPlate* pp = partplate_list.get_curr_plate(); - BuildVolume build_volume(pp->get_shape(), this->bed.build_volume().printable_height(), this->bed.build_volume().extruder_areas(), this->bed.build_volume().extruder_heights()); + BuildVolume build_volume(pp->get_shape(), this->bed.build_volume().printable_height(), this->bed.build_volume().extruder_areas(), + this->bed.build_volume().extruder_heights()); this->model.update_print_volume_state(build_volume); } -void Plater::priv::process_validation_warning(StringObjectException const &warning) const +void Plater::priv::process_validation_warning(StringObjectException const& warning) const { if (warning.string.empty()) notification_manager->close_notification_of_type(NotificationType::ValidateWarning); else { std::string text = warning.string; - auto po = dynamic_cast(warning.object); - auto mo = po ? po->model_object() : dynamic_cast(warning.object); - //ORCA: Update process_validation_warning to handle ModelInstance selection and include fallback - auto mi = dynamic_cast(warning.object); + auto po = dynamic_cast(warning.object); + auto mo = po ? po->model_object() : dynamic_cast(warning.object); + // ORCA: Update process_validation_warning to handle ModelInstance selection and include fallback + auto mi = dynamic_cast(warning.object); - auto action_fn = (mo || mi || !warning.opt_key.empty()) ? [id = mo ? mo->id() : (mi ? mi->id() : 0), - parent_id = mi ? mi->get_object()->id() : 0, - is_inst = (mi != nullptr), - opt = warning.opt_key](wxEvtHandler *) { - auto & objects = wxGetApp().model().objects; + auto action_fn = (mo || mi || !warning.opt_key.empty()) ? + [id = mo ? mo->id() : (mi ? mi->id() : 0), parent_id = mi ? mi->get_object()->id() : 0, + is_inst = (mi != nullptr), opt = warning.opt_key](wxEvtHandler*) { + auto& objects = wxGetApp().model().objects; - if (is_inst) { - bool selected = false; - auto iter = std::find_if(objects.begin(), objects.end(), [parent_id](auto o) { return o->id() == parent_id; }); - if (iter != objects.end()) { - ModelObject* obj = *iter; - int inst_idx = -1; - for(size_t i=0; iinstances.size(); ++i) { - if (obj->instances[i]->id() == id) { - inst_idx = i; - break; - } - } + if (is_inst) { + bool selected = false; + auto iter = std::find_if(objects.begin(), objects.end(), + [parent_id](auto o) { return o->id() == parent_id; }); + if (iter != objects.end()) { + ModelObject* obj = *iter; + int inst_idx = -1; + for (size_t i = 0; i < obj->instances.size(); ++i) { + if (obj->instances[i]->id() == id) { + inst_idx = i; + break; + } + } - wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - if (inst_idx != -1) { - auto* model = wxGetApp().obj_list()->GetModel(); - wxDataViewItem item; - wxDataViewItem objItem = model->GetObjectItem(obj); - if (objItem.IsOk()) { - int vm_obj_idx = model->GetIdByItem(objItem); - if (vm_obj_idx != -1) { - item = model->GetItemByInstanceId(vm_obj_idx, inst_idx); - } - } - if (item.IsOk()) { - wxDataViewItemArray sel_items; - sel_items.Add(item); - wxGetApp().obj_list()->select_items(sel_items); - wxGetApp().obj_list()->update_selections_on_canvas(); - selected = true; - } - } + if (inst_idx != -1) { + auto* model = wxGetApp().obj_list()->GetModel(); + wxDataViewItem item; + wxDataViewItem objItem = model->GetObjectItem(obj); + if (objItem.IsOk()) { + int vm_obj_idx = model->GetIdByItem(objItem); + if (vm_obj_idx != -1) { + item = model->GetItemByInstanceId(vm_obj_idx, inst_idx); + } + } + if (item.IsOk()) { + wxDataViewItemArray sel_items; + sel_items.Add(item); + wxGetApp().obj_list()->select_items(sel_items); + wxGetApp().obj_list()->update_selections_on_canvas(); + selected = true; + } + } - if (!selected) { - wxGetApp().obj_list()->select_items({ {obj, nullptr} }); - wxGetApp().obj_list()->update_selections_on_canvas(); - } - } - } else { - auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); - if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - wxGetApp().obj_list()->select_items({{*iter, nullptr}}); - wxGetApp().obj_list()->update_selections_on_canvas(); - } - } - if (!opt.empty()) { - if ((!is_inst && id.id) || (is_inst && parent_id.id)) - wxGetApp().params_panel()->switch_to_object(); - wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); - } - return false; - } : std::function(); + if (!selected) { + wxGetApp().obj_list()->select_items({{obj, nullptr}}); + wxGetApp().obj_list()->update_selections_on_canvas(); + } + } + } else { + auto iter = id.id ? + std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : + objects.end(); + if (iter != objects.end()) { + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); + wxGetApp().obj_list()->select_items({{*iter, nullptr}}); + wxGetApp().obj_list()->update_selections_on_canvas(); + } + } + if (!opt.empty()) { + if ((!is_inst && id.id) || (is_inst && parent_id.id)) + wxGetApp().params_panel()->switch_to_object(); + wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); + } + return false; + } : + std::function(); auto hypertext = (mo || mi || !warning.opt_key.empty()) ? _u8L("Jump to") : ""; - if (mo) hypertext += std::string(" [") + mo->name + "]"; - if (mi) hypertext += std::string(" [") + mi->get_object()->name + "]"; - if (!warning.opt_key.empty()) hypertext += std::string(" (") + warning.opt_key + ")"; + if (mo) + hypertext += std::string(" [") + mo->name + "]"; + if (mi) + hypertext += std::string(" [") + mi->get_object()->name + "]"; + if (!warning.opt_key.empty()) + hypertext += std::string(" (") + warning.opt_key + ")"; // BBS disable support enforcer - //if (text == "_SUPPORTS_OFF") { + // if (text == "_SUPPORTS_OFF") { // text = _u8L("An object has custom support enforcers which will not be used " // "because supports are disabled.")+"\n"; // hypertext = _u8L("Enable supports for enforcers only"); @@ -9010,24 +10675,21 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni // }; //} - notification_manager->push_notification( - NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + text, hypertext, action_fn - ); + notification_manager->push_notification(NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + text, hypertext, action_fn); } } -void Plater::priv::process_validation_warnings(const std::vector &warnings) const +void Plater::priv::process_validation_warnings(const std::vector& warnings) const { // ValidateWarning stacks by text (m_multiple_types), so clear the stale set before re-adding. notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - for (const StringObjectException &warning : warnings) + for (const StringObjectException& warning : warnings) if (!warning.string.empty()) process_validation_warning(warning); } - // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) @@ -9041,50 +10703,52 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // Update the "out of print bed" state of ModelInstances. update_print_volume_state(); // Apply new config to the possibly running background task. - bool was_running = background_process.running(); - //BBS: add the switch print logic before Print::Apply - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, force_validation=%1% postpone_error_messages=%2%, switch_print=%3%, was_running=%4%")%force_validation %postpone_error_messages %switch_print %was_running; - if (switch_print) - { - //BBS: update the current print to the current plate + bool was_running = background_process.running(); + // BBS: add the switch print logic before Print::Apply + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format( + ": enter, force_validation=%1% postpone_error_messages=%2%, switch_print=%3%, was_running=%4%") % + force_validation % postpone_error_messages % switch_print % was_running; + if (switch_print) { + // BBS: update the current print to the current plate this->partplate_list.update_slice_context_to_current_plate(background_process); this->preview->update_gcode_result(partplate_list.get_current_slice_result()); } - background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); + background_process.fff_print()->set_check_multi_filaments_compatibility( + wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); Print::ApplyStatus invalidated; const auto& preset_bundle = wxGetApp().preset_bundle; if (preset_bundle->get_printer_extruder_count() > 1) { - PartPlate* cur_plate = background_process.get_current_plate(); - std::vector f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config); + PartPlate* cur_plate = background_process.get_current_plate(); + std::vector f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = cur_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps, f_volume_maps)); background_process.fff_print()->set_extruder_filament_info(get_extruder_filament_info()); - } - else + } else invalidated = background_process.apply(this->model, preset_bundle->full_config(false)); if ((invalidated == Print::APPLY_STATUS_CHANGED) || (invalidated == Print::APPLY_STATUS_INVALIDATED)) // BBS: add only gcode mode q->set_only_gcode(false); - //BBS: add slicing related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process apply result=%1%")%invalidated; + // BBS: add slicing related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process apply result=%1%") % invalidated; if (background_process.empty()) view3D->get_canvas3d()->reset_sequential_print_clearance(); if (invalidated == Print::APPLY_STATUS_INVALIDATED) { - //BBS: update current plater's slicer result to invalid + // BBS: update current plater's slicer result to invalid this->background_process.get_current_plate()->update_slice_result_valid_state(false); - //no need, should be done in background_process.apply - //this->background_process.get_current_gcode_result()->reset(); - // Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared. - // Otherwise they will be just refreshed. + // no need, should be done in background_process.apply + // this->background_process.get_current_gcode_result()->reset(); + // Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared. + // Otherwise they will be just refreshed. if (preview != nullptr) { // If the preview is not visible, the following line just invalidates the preview, // but the G-code paths or SLA preview are calculated first once the preview is made visible. @@ -9097,28 +10761,27 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; notification_manager->set_slicing_progress_hidden(); - } - else { + } else { if (preview && preview->get_reload_paint_after_background_process_apply()) { preview->set_reload_paint_after_background_process_apply(false); preview->reload_print(); } } - if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && ! background_process.empty()) { + if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && !background_process.empty()) { // The delayed error message is no more valid. delayed_error_message.clear(); // The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors. - //BBS: add is_warning logic + // BBS: add is_warning logic std::vector warnings; - //BBS: refine seq-print logic + // BBS: refine seq-print logic Polygons polygons; std::vector> height_polygons; StringObjectException err = background_process.validate(&warnings, &polygons, &height_polygons); // update string by type q->post_process_string_object_exception(err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%")%err.string%warnings.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%") % err.string % warnings.size(); if (err.string.empty()) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -9135,92 +10798,88 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool view3D->get_canvas3d()->set_as_dirty(); view3D->get_canvas3d()->request_extra_frame(); } - } - else { + } else { this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); // The print is not valid. // Show error as notification. notification_manager->push_validate_error_notification(err); - //also update the warnings + // also update the warnings process_validation_warnings(warnings); return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; if (printer_technology == ptFFF) { const Print* print = background_process.fff_print(); - //Polygons polygons; - //if (print->config().print_sequence == PrintSequence::ByObject) - // Print::sequential_print_clearance_valid(*print, &polygons); + // Polygons polygons; + // if (print->config().print_sequence == PrintSequence::ByObject) + // Print::sequential_print_clearance_valid(*print, &polygons); view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); view3D->get_canvas3d()->set_sequential_print_clearance_render_fill(true); view3D->get_canvas3d()->set_sequential_print_clearance_polygons(polygons, height_polygons); } } - } - else if (! this->delayed_error_message.empty()) { + } else if (!this->delayed_error_message.empty()) { // Reusing the old state. return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } - //actualizate warnings + // actualizate warnings if (invalidated != Print::APPLY_STATUS_UNCHANGED || background_process.empty()) { if (background_process.empty()) process_validation_warning({}); actualize_slicing_warnings(*this->background_process.current_print()); actualize_object_warnings(*this->background_process.current_print()); - show_warning_dialog = false; + show_warning_dialog = false; process_completed_with_error = -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: was_running = %2%, running %3%, invalidated=%4%, return_state=%5%, internal_cancel=%6%") - % __LINE__ % was_running % this->background_process.running() % invalidated % return_state % this->background_process.is_internal_cancelled(); - if (was_running && ! this->background_process.running() && (return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) { - if (invalidated != Print::APPLY_STATUS_UNCHANGED || this->background_process.is_internal_cancelled()) - { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format( + ", Line %1%: was_running = %2%, running %3%, invalidated=%4%, return_state=%5%, internal_cancel=%6%") % + __LINE__ % was_running % this->background_process.running() % invalidated % return_state % + this->background_process.is_internal_cancelled(); + if (was_running && !this->background_process.running() && (return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) { + if (invalidated != Print::APPLY_STATUS_UNCHANGED || this->background_process.is_internal_cancelled()) { // The background processing was killed and it will not be restarted. // Post the "canceled" callback message, so that it will be processed after any possible pending status bar update messages. - SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, - SlicingProcessCompletedEvent::Cancelled, nullptr); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%, post an EVT_PROCESS_COMPLETED to main, status %2%")%__LINE__ %evt.status(); + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Cancelled, nullptr); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" %1%, post an EVT_PROCESS_COMPLETED to main, status %2%") % __LINE__ % evt.status(); wxQueueEvent(q, evt.Clone()); } } - if ((return_state & UPDATE_BACKGROUND_PROCESS_INVALID) != 0) - { + if ((return_state & UPDATE_BACKGROUND_PROCESS_INVALID) != 0) { // Validation of the background data failed. - //BBS: add slice&&print status update logic + // BBS: add slice&&print status update logic this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); process_completed_with_error = partplate_list.get_curr_plate_index(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: set to process_completed_with_error, return_state=%2%")%__LINE__%return_state; - } - else - { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(", Line %1%: set to process_completed_with_error, return_state=%2%") % __LINE__ % + return_state; + } else { // Background data is valid. - if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || - (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0 ) + if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0) notification_manager->set_slicing_progress_hidden(); - //BBS: add slice&&print status update logic - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: background data valid, return_state=%2%")%__LINE__%return_state; + // BBS: add slice&&print status update logic + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(", Line %1%: background data valid, return_state=%2%") % __LINE__ % return_state; PartPlate* cur_plate = background_process.get_current_plate(); - if (background_process.finished() && cur_plate && cur_plate->is_slice_result_valid()) - { - //ready_to_slice = false; + if (background_process.finished() && cur_plate && cur_plate->is_slice_result_valid()) { + // ready_to_slice = false; this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); - } - else if (!background_process.empty() && - !background_process.running()) /* Do not update buttons if background process is running - * This condition is important for SLA mode especially, - * when this function is called several times during calculations - * */ + } else if (!background_process.empty() && + !background_process.running()) /* Do not update buttons if background process is running + * This condition is important for SLA mode especially, + * when this function is called several times during calculations + * */ { if (cur_plate->can_slice()) { - //ready_to_slice = true; + // ready_to_slice = true; this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true); process_completed_with_error = -1; - } - else { - //ready_to_slice = false; + } else { + // ready_to_slice = false; this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); process_completed_with_error = partplate_list.get_curr_plate_index(); } @@ -9244,7 +10903,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool #endif } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: exit, return_state=%2%")%__LINE__%return_state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: exit, return_state=%2%") % __LINE__ % return_state; return return_state; } @@ -9253,36 +10912,33 @@ bool Plater::priv::restart_background_process(unsigned int state) { if (!m_worker.is_idle()) { // Avoid a race condition - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: ui jobs running, return false")%__LINE__; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: ui jobs running, return false") % __LINE__; return false; } - if ( ! this->background_process.empty() && - (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 && - ( ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && ! this->background_process.finished()) || - (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || - (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: print is valid, try to start it now")%__LINE__; + if (!this->background_process.empty() && (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 && + (((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && !this->background_process.finished()) || + (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: print is valid, try to start it now") % __LINE__; // The print is valid and it can be started. if (this->background_process.start()) { if (!show_warning_dialog) on_slicing_began(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully")%__LINE__; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully") % __LINE__; return true; } - } - else if (this->background_process.empty()) { + } else if (this->background_process.empty()) { PartPlate* cur_plate = background_process.get_current_plate(); if (cur_plate->is_slice_result_valid() && ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0)) { if (this->background_process.start()) { if (!show_warning_dialog) on_slicing_began(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully")%__LINE__; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully") % __LINE__; return true; } } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: not started")%__LINE__; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: not started") % __LINE__; return false; } @@ -9290,7 +10946,7 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova { wxCHECK_RET(!(output_path.empty()), "export_gcode: output_path and upload_job empty"); - BOOST_LOG_TRIVIAL(trace) << boost::format("export_gcode: output_path %1%")%output_path.string(); + BOOST_LOG_TRIVIAL(trace) << boost::format("export_gcode: output_path %1%") % output_path.string(); if (model.objects.empty()) return; @@ -9308,9 +10964,9 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova return; show_warning_dialog = true; - if (! output_path.empty()) { + if (!output_path.empty()) { background_process.schedule_export(output_path.string(), output_path_on_removable_media); - notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0); + notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() { return true; }, 1000, 0); } else { BOOST_LOG_TRIVIAL(info) << "output_path is empty"; } @@ -9340,9 +10996,9 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova return; show_warning_dialog = true; - if (! output_path.empty()) { + if (!output_path.empty()) { background_process.schedule_export(output_path.string(), output_path_on_removable_media); - notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0); + notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() { return true; }, 1000, 0); } else { background_process.schedule_upload(std::move(upload_job)); } @@ -9354,10 +11010,9 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova unsigned int Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview) { bool switch_print = true; - //BBS: judge whether can switch print or not - if ((partplate_list.get_plate_count() > 1) && !this->background_process.can_switch_print()) - { - //can not switch print currently + // BBS: judge whether can switch print or not + if ((partplate_list.get_plate_count() > 1) && !this->background_process.can_switch_print()) { + // can not switch print currently BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate count %1%, can not switch") % partplate_list.get_plate_count(); switch_print = false; } @@ -9378,21 +11033,19 @@ void Plater::priv::update_fff_scene() this->preview->reload_print(); // In case this was MM print, wipe tower bounding box on 3D tab might need redrawing with exact depth: view3D->reload_scene(true); - //BBS: add assemble view related logic + // BBS: add assemble view related logic assemble_view->reload_scene(true); q->mark_plate_toolbar_image_dirty(); } -//BBS: add print project related logic +// BBS: add print project related logic void Plater::priv::update_fff_scene_only_shells(bool only_shells) { - if (this->preview != nullptr) - { + if (this->preview != nullptr) { const Print* current_print = this->background_process.fff_print(); - if (current_print) - { - //this->preview->reset_shells(); + if (current_print) { + // this->preview->reset_shells(); this->preview->load_shells(*current_print); } } @@ -9420,13 +11073,14 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const try { const bool is_step = boost::algorithm::iends_with(path, ".stp") || boost::algorithm::iends_with(path, ".step"); if (is_step) { - auto config = wxGetApp().app_config; - double linear = std::max(0.003, string_to_double_decimal_point(config->get("linear_deflection"))); - double angle = std::max(0.5, string_to_double_decimal_point(config->get("angle_deflection"))); + auto config = wxGetApp().app_config; + double linear = std::max(0.003, string_to_double_decimal_point(config->get("linear_deflection"))); + double angle = std::max(0.5, string_to_double_decimal_point(config->get("angle_deflection"))); bool split_compound = config->get_bool("is_split_compound"); bool is_user_cancel = false; - auto callback = [&is_user_cancel, linear, angle, split_compound](Slic3r::Step &file, double &linear_value, double &angle_value, bool &is_split) -> int { + auto callback = [&is_user_cancel, linear, angle, split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, + bool& is_split) -> int { if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { StepMeshDialog mesh_dlg(nullptr, file, linear, angle); if (mesh_dlg.ShowModal() == wxID_OK) { @@ -9445,8 +11099,10 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const return -1; }; - new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, callback, linear, angle, split_compound); - if (is_user_cancel) return false; + new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, callback, + linear, angle, split_compound); + if (is_user_cancel) + return false; } else { new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel); } @@ -9454,14 +11110,14 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const model_object->center_around_origin(); model_object->ensure_on_bed(); } - } - catch (std::exception&) { + } catch (std::exception&) { // error while loading return false; } if (new_model.objects.size() > 1 || new_model.objects.front()->volumes.size() > 1) { - MessageDialog dlg(q, _L("Unable to replace with more than one volume"), _L("Error during replacement"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + MessageDialog dlg(q, _L("Unable to replace with more than one volume"), _L("Error during replacement"), + wxOK | wxOK_DEFAULT | wxICON_WARNING); dlg.ShowModal(); return false; } @@ -9472,7 +11128,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const q->take_snapshot(snapshot); ModelObject* old_model_object = model.objects[object_idx]; - ModelVolume* old_volume = old_model_object->volumes[volume_idx]; + ModelVolume* old_volume = old_model_object->volumes[volume_idx]; bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; @@ -9484,7 +11140,8 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * + (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); @@ -9524,7 +11181,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const void Plater::priv::replace_with_stl() { - if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + if (!q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) return; const Selection& selection = get_selection(); @@ -9533,8 +11190,8 @@ void Plater::priv::replace_with_stl() return; const GLVolume* v = selection.get_first_volume(); - int object_idx = v->object_idx(); - int volume_idx = v->volume_idx(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); // collects paths of files to load @@ -9553,7 +11210,8 @@ void Plater::priv::replace_with_stl() fs::path out_path = dialog.GetPath().ToUTF8().data(); if (out_path.empty()) { - MessageDialog dlg(q, _L("File for the replacement wasn\'t selected"), _L("Error during replacement"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + MessageDialog dlg(q, _L("File for the replacement wasn\'t selected"), _L("Error during replacement"), + wxOK | wxOK_DEFAULT | wxICON_WARNING); dlg.ShowModal(); return; } @@ -9572,7 +11230,7 @@ void Plater::priv::replace_with_stl() void Plater::priv::replace_all_with_stl() { - if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + if (!q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) return; const Selection& selection = get_selection(); @@ -9596,7 +11254,7 @@ void Plater::priv::replace_all_with_stl() Slic3r::GUI::ItemType item_type = wxGetApp().obj_list()->GetModel()->GetItemType(item); if (item_type & itPlate) { if (item.IsOk()) { - ObjectDataViewModelNode *node = static_cast(item.GetID()); + ObjectDataViewModelNode* node = static_cast(item.GetID()); selected_plate_idxs.push_back(node->GetPlateIdx()); } } @@ -9617,8 +11275,8 @@ void Plater::priv::replace_all_with_stl() for (unsigned int idx : volume_idxs) { const GLVolume* v = selection.get_volume(idx); - int object_idx = v->object_idx(); - int volume_idx = v->volume_idx(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); const ModelObject* object = model.objects[object_idx]; const ModelVolume* volume = object->volumes[volume_idx]; @@ -9637,7 +11295,8 @@ void Plater::priv::replace_all_with_stl() fs::path out_path = dialog.GetPath().ToUTF8().data(); if (out_path.empty()) { - MessageDialog dlg(q, _L("Directory for the replace wasn't selected"), _L("Error during replacement"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + MessageDialog dlg(q, _L("Directory for the replace wasn't selected"), _L("Error during replacement"), + wxOK | wxOK_DEFAULT | wxICON_WARNING); dlg.ShowModal(); return; } @@ -9646,8 +11305,8 @@ void Plater::priv::replace_all_with_stl() for (unsigned int idx : volume_idxs) { const GLVolume* v = selection.get_volume(idx); - int object_idx = v->object_idx(); - int volume_idx = v->volume_idx(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); const ModelObject* object = model.objects[object_idx]; const ModelVolume* volume = object->volumes[volume_idx]; @@ -9697,19 +11356,20 @@ void Plater::priv::replace_all_with_stl() } #if ENABLE_RELOAD_FROM_DISK_REWORK -static std::vector> reloadable_volumes(const Model &model, const Selection &selection) +static std::vector> reloadable_volumes(const Model& model, const Selection& selection) { std::vector> ret; - const std::set & selected_volumes_idxs = selection.get_volume_idxs(); + const std::set& selected_volumes_idxs = selection.get_volume_idxs(); for (unsigned int idx : selected_volumes_idxs) { - const GLVolume &v = *selection.get_volume(idx); - const int o_idx = v.object_idx(); + const GLVolume& v = *selection.get_volume(idx); + const int o_idx = v.object_idx(); if (0 <= o_idx && o_idx < int(model.objects.size())) { - const ModelObject *obj = model.objects[o_idx]; - const int v_idx = v.volume_idx(); + const ModelObject* obj = model.objects[o_idx]; + const int v_idx = v.volume_idx(); if (0 <= v_idx && v_idx < int(obj->volumes.size())) { - const ModelVolume *vol = obj->volumes[v_idx]; - if (!vol->source.is_from_builtin_objects && !vol->source.input_file.empty() && !fs::path(vol->source.input_file).extension().string().empty()) + const ModelVolume* vol = obj->volumes[v_idx]; + if (!vol->source.is_from_builtin_objects && !vol->source.input_file.empty() && + !fs::path(vol->source.input_file).extension().string().empty()) ret.push_back({o_idx, v_idx}); } } @@ -9728,12 +11388,14 @@ void Plater::priv::reload_from_disk() if (selected_volumes.empty()) return; - std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair& v1, const std::pair& v2) { return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second); - }); - selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { - return (v1.first == v2.first) && (v1.second == v2.second); - }), selected_volumes.end()); + }); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), + [](const std::pair& v1, const std::pair& v2) { + return (v1.first == v2.first) && (v1.second == v2.second); + }), + selected_volumes.end()); #else Plater::TakeSnapshot snapshot(q, _u8L("Reload from disk")); @@ -9749,8 +11411,9 @@ void Plater::priv::reload_from_disk() int volume_idx; // operators needed by std::algorithms - bool operator < (const SelectedVolume& other) const { return object_idx < other.object_idx || (object_idx == other.object_idx && volume_idx < other.volume_idx); } - bool operator == (const SelectedVolume& other) const { return object_idx == other.object_idx && volume_idx == other.volume_idx; } + bool operator<(const SelectedVolume& other) const + { return object_idx < other.object_idx || (object_idx == other.object_idx && volume_idx < other.volume_idx); } + bool operator==(const SelectedVolume& other) const { return object_idx == other.object_idx && volume_idx == other.volume_idx; } }; std::vector selected_volumes; @@ -9758,11 +11421,11 @@ void Plater::priv::reload_from_disk() const std::set& selected_volumes_idxs = selection.get_volume_idxs(); for (unsigned int idx : selected_volumes_idxs) { const GLVolume* v = selection.get_volume(idx); - int v_idx = v->volume_idx(); + int v_idx = v->volume_idx(); if (v_idx >= 0) { int o_idx = v->object_idx(); - if (0 <= o_idx && o_idx < (int)model.objects.size()) - selected_volumes.push_back({ o_idx, v_idx }); + if (0 <= o_idx && o_idx < (int) model.objects.size()) + selected_volumes.push_back({o_idx, v_idx}); } } std::sort(selected_volumes.begin(), selected_volumes.end()); @@ -9775,8 +11438,8 @@ void Plater::priv::reload_from_disk() #if ENABLE_RELOAD_FROM_DISK_REWORK std::vector> replace_paths; for (auto [obj_idx, vol_idx] : selected_volumes) { - const ModelObject *object = model.objects[obj_idx]; - const ModelVolume *volume = object->volumes[vol_idx]; + const ModelObject* object = model.objects[obj_idx]; + const ModelVolume* volume = object->volumes[vol_idx]; if (fs::exists(volume->source.input_file)) input_paths.push_back(volume->source.input_file); else { @@ -9822,8 +11485,8 @@ void Plater::priv::reload_from_disk() if (!found) missing_input_paths.push_back(volume->source.input_file); } - } - else if (!object->input_file.empty() && volume->is_model_part() && !volume->name.empty() && !volume->source.is_from_builtin_objects) + } else if (!object->input_file.empty() && volume->is_model_part() && !volume->name.empty() && + !volume->source.is_from_builtin_objects) missing_input_paths.push_back(volume->name); } #endif // ENABLE_RELOAD_FROM_DISK_REWORK @@ -9834,7 +11497,7 @@ void Plater::priv::reload_from_disk() while (!missing_input_paths.empty()) { // ask user to select the missing file fs::path search = missing_input_paths.back(); - wxString title = _L("Please select a file"); + wxString title = _L("Please select a file"); #if defined(__APPLE__) title += " (" + from_u8(search.filename().string()) + ")"; #endif // __APPLE__ @@ -9844,7 +11507,7 @@ void Plater::priv::reload_from_disk() return; std::string sel_filename_path = dialog.GetPath().ToUTF8().data(); - std::string sel_filename = fs::path(sel_filename_path).filename().string(); + std::string sel_filename = fs::path(sel_filename_path).filename().string(); if (boost::algorithm::iequals(search.filename().string(), sel_filename)) { input_paths.push_back(sel_filename_path); missing_input_paths.pop_back(); @@ -9859,13 +11522,11 @@ void Plater::priv::reload_from_disk() if (fs::exists(repathed_filename)) { input_paths.push_back(repathed_filename.string()); it = missing_input_paths.erase(it); - } - else + } else ++it; } - } - else { - wxString message = _L("Do you want to replace it") + " ?"; + } else { + wxString message = _L("Do you want to replace it") + " ?"; MessageDialog dlg(q, message, _L("Message"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION); if (dlg.ShowModal() == wxID_YES) #if ENABLE_RELOAD_FROM_DISK_REWORK @@ -9891,11 +11552,13 @@ void Plater::priv::reload_from_disk() // load one file at a time for (size_t i = 0; i < input_paths.size(); ++i) { - const auto& path = input_paths[i].string(); - auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { - if (!boost::iends_with(path, ".obj")) { return; } + const auto& path = input_paths[i].string(); + auto obj_color_fun = [this, &path](ObjDialogInOut& in_out) { + if (!boost::iends_with(path, ".obj")) { + return; + } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } @@ -9904,72 +11567,68 @@ void Plater::priv::reload_from_disk() wxBusyInfo info(_L("Reload from:") + " " + from_u8(path), q->get_current_canvas3D()->get_wxglcanvas()); Model new_model; - try - { - //BBS: add plate data related logic + try { + // BBS: add plate data related logic PlateDataPtrs plate_data; - //BBS: project embedded settings + // BBS: project embedded settings std::vector project_presets; // BBS: backup - if (boost::iends_with(path, ".stp") || - boost::iends_with(path, ".step")) { + if (boost::iends_with(path, ".stp") || boost::iends_with(path, ".step")) { double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_deflection")); - double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_deflection")); - bool is_split = wxGetApp().app_config->get_bool("is_split_compound"); - new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, nullptr, linear, angle, is_split); - }else { - new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, obj_color_fun); + double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_deflection")); + bool is_split = wxGetApp().app_config->get_bool("is_split_compound"); + new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, + nullptr, linear, angle, is_split); + } else { + new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, + &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, + obj_color_fun); } - - for (ModelObject* model_object : new_model.objects) - { + for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); } - if (plate_data.size() > 0) - { - //partplate_list.load_from_3mf_structure(plate_data); + if (plate_data.size() > 0) { + // partplate_list.load_from_3mf_structure(plate_data); partplate_list.update_slice_context_to_current_plate(background_process); this->preview->update_gcode_result(partplate_list.get_current_slice_result()); release_PlateData_list(plate_data); sidebar->obj_list()->reload_all_plates(); } - } - catch (std::exception&) - { + } catch (std::exception&) { // error while loading return; } #if ENABLE_RELOAD_FROM_DISK_REWORK for (auto [obj_idx, vol_idx] : selected_volumes) { - ModelObject *old_model_object = model.objects[obj_idx]; - ModelVolume *old_volume = old_model_object->volumes[vol_idx]; + ModelObject* old_model_object = model.objects[obj_idx]; + ModelVolume* old_volume = old_model_object->volumes[vol_idx]; bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; bool has_source = !old_volume->source.input_file.empty() && - boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); - bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); + boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), + fs::path(path).filename().string()); + bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); if (has_source || has_name) { - int new_volume_idx = -1; - int new_object_idx = -1; - bool match_found = false; + int new_volume_idx = -1; + int new_object_idx = -1; + bool match_found = false; // take idxs from the matching volume if (has_source && old_volume->source.object_idx < int(new_model.objects.size())) { - const ModelObject *obj = new_model.objects[old_volume->source.object_idx]; + const ModelObject* obj = new_model.objects[old_volume->source.object_idx]; if (old_volume->source.volume_idx < int(obj->volumes.size())) { - const std::string &new_input_file = obj->volumes[old_volume->source.volume_idx]->source.input_file; - const std::string &old_input_file = old_volume->source.input_file; + const std::string& new_input_file = obj->volumes[old_volume->source.volume_idx]->source.input_file; + const std::string& old_input_file = old_volume->source.input_file; // Orca: match on the exact source path first, then fall back to filename-only so reload // still matches when the project stored a bare filename and the file was found next to // the project (same-folder fallback) or picked via the locate dialog (#12992). - if (new_input_file == old_input_file || - boost::algorithm::iequals(fs::path(new_input_file).filename().string(), - fs::path(old_input_file).filename().string())) { + if (new_input_file == old_input_file || boost::algorithm::iequals(fs::path(new_input_file).filename().string(), + fs::path(old_input_file).filename().string())) { new_volume_idx = old_volume->source.volume_idx; new_object_idx = old_volume->source.object_idx; match_found = true; @@ -9980,8 +11639,8 @@ void Plater::priv::reload_from_disk() if (!match_found && has_name) { // take idxs from the 1st matching volume for (size_t o = 0; o < new_model.objects.size(); ++o) { - ModelObject *obj = new_model.objects[o]; - bool found = false; + ModelObject* obj = new_model.objects[o]; + bool found = false; for (size_t v = 0; v < obj->volumes.size(); ++v) { if (obj->volumes[v]->name == old_volume->name) { new_volume_idx = (int) v; @@ -9990,7 +11649,8 @@ void Plater::priv::reload_from_disk() break; } } - if (found) break; + if (found) + break; // BBS: step model,object loaded as a volume. GUI_ObfectList.cpp load_modifier() if (obj->name == old_volume->name) { new_object_idx = (int) o; @@ -10003,24 +11663,24 @@ void Plater::priv::reload_from_disk() fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); continue; } - ModelObject *new_model_object = new_model.objects[new_object_idx]; + ModelObject* new_model_object = new_model.objects[new_object_idx]; if (int(new_model_object->volumes.size()) <= new_volume_idx) { fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); continue; } - ModelVolume *new_volume = nullptr; + ModelVolume* new_volume = nullptr; // BBS: step model if (new_volume_idx < 0 && new_object_idx >= 0) { - TriangleMesh mesh = new_model_object->mesh(); - new_volume = old_model_object->add_volume(std::move(mesh)); - new_volume->name = new_model_object->name; + TriangleMesh mesh = new_model_object->mesh(); + new_volume = old_model_object->add_volume(std::move(mesh)); + new_volume->name = new_model_object->name; new_volume->source.input_file = new_model_object->input_file; - }else { + } else { new_volume = old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); // new_volume = old_model_object->volumes.back(); } - + new_volume->set_new_unique_id(); new_volume->config.apply(old_volume->config); new_volume->set_type(old_volume->type()); @@ -10048,7 +11708,8 @@ void Plater::priv::reload_from_disk() std::swap(old_model_object->volumes[vol_idx], old_model_object->volumes.back()); old_model_object->delete_volume(old_model_object->volumes.size() - 1); - if (!sinking) old_model_object->ensure_on_bed(); + if (!sinking) + old_model_object->ensure_on_bed(); old_model_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); sla::reproject_points_and_holes(old_model_object); @@ -10061,37 +11722,39 @@ void Plater::priv::reload_from_disk() // update the selected volumes whose source is the current file for (const SelectedVolume& sel_v : selected_volumes) { ModelObject* old_model_object = model.objects[sel_v.object_idx]; - ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; + ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; bool sinking = old_model_object->bounding_box().min.z() < SINKING_Z_THRESHOLD; - bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); - bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); + bool has_source = !old_volume->source.input_file.empty() && + boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), + fs::path(path).filename().string()); + bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); if (has_source || has_name) { int new_volume_idx = -1; int new_object_idx = -1; -// if (has_source) { -// // take idxs from source -// new_volume_idx = old_volume->source.volume_idx; -// new_object_idx = old_volume->source.object_idx; -// } -// else { - // take idxs from the 1st matching volume - for (size_t o = 0; o < new_model.objects.size(); ++o) { - ModelObject* obj = new_model.objects[o]; - bool found = false; - for (size_t v = 0; v < obj->volumes.size(); ++v) { - if (obj->volumes[v]->name == old_volume->name) { - new_volume_idx = (int)v; - new_object_idx = (int)o; - found = true; - break; - } - } - if (found) + // if (has_source) { + // // take idxs from source + // new_volume_idx = old_volume->source.volume_idx; + // new_object_idx = old_volume->source.object_idx; + // } + // else { + // take idxs from the 1st matching volume + for (size_t o = 0; o < new_model.objects.size(); ++o) { + ModelObject* obj = new_model.objects[o]; + bool found = false; + for (size_t v = 0; v < obj->volumes.size(); ++v) { + if (obj->volumes[v]->name == old_volume->name) { + new_volume_idx = (int) v; + new_object_idx = (int) o; + found = true; break; + } } -// } + if (found) + break; + } + // } if (new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) { fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); @@ -10110,10 +11773,11 @@ void Plater::priv::reload_from_disk() new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * + (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); new_volume->source.object_idx = old_volume->source.object_idx; new_volume->source.volume_idx = old_volume->source.volume_idx; - assert(! old_volume->source.is_converted_from_inches || ! old_volume->source.is_converted_from_meters); + assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); else if (old_volume->source.is_converted_from_meters) @@ -10134,8 +11798,10 @@ void Plater::priv::reload_from_disk() for (auto [src, dest] : replace_paths) { for (auto [obj_idx, vol_idx] : selected_volumes) { if (boost::algorithm::iequals(model.objects[obj_idx]->volumes[vol_idx]->source.input_file, src.string())) - // When an error occurs, either the dest parsing error occurs, or the number of objects in the dest is greater than 1 and cannot be replaced, and cannot be replaced in this loop. - if (!replace_volume_with_stl(obj_idx, vol_idx, dest, "")) break; + // When an error occurs, either the dest parsing error occurs, or the number of objects in the dest is greater than 1 and + // cannot be replaced, and cannot be replaced in this loop. + if (!replace_volume_with_stl(obj_idx, vol_idx, dest, "")) + break; } } #else @@ -10143,8 +11809,10 @@ void Plater::priv::reload_from_disk() const auto& path = replace_paths[i].string(); for (const SelectedVolume& sel_v : selected_volumes) { ModelObject* old_model_object = model.objects[sel_v.object_idx]; - ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; - bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); + ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; + bool has_source = !old_volume->source.input_file.empty() && + boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), + fs::path(path).filename().string()); if (!replace_volume_with_stl(sel_v.object_idx, sel_v.volume_idx, path, "")) { fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); } @@ -10180,7 +11848,7 @@ void Plater::priv::reload_all_from_disk() Plater::TakeSnapshot snapshot(q, _u8L("Reload all")); Plater::SuppressSnapshots suppress(q); - Selection& selection = get_selection(); + Selection& selection = get_selection(); Selection::IndicesList curr_idxs = selection.get_volume_idxs(); // reload from disk uses selection select_all(); @@ -10192,90 +11860,88 @@ void Plater::priv::reload_all_from_disk() } } -//BBS: add no_slice logic +// BBS: add no_slice logic void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) { if (std::find(panels.begin(), panels.end(), panel) == panels.end()) return; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current_panel %1%, new_panel %2%")%current_panel%panel; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current_panel %1%, new_panel %2%") % current_panel % panel; #ifdef __WXMAC__ bool force_render = (current_panel != nullptr); #endif // __WXMAC__ - //BBS: add slice logic when switch to preview page + // BBS: add slice logic when switch to preview page auto do_reslice = [this, no_slice]() { - // see: Plater::priv::object_list_changed() - // FIXME: it may be better to have a single function making this check and let it be called wherever needed - bool export_in_progress = this->background_process.is_export_scheduled(); - ObjectFilamentResults object_results; - bool model_fits = this->view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; - model_fits = model_fits&&object_results.filaments.empty(); - //BBS: add partplate logic - PartPlate * current_plate = this->partplate_list.get_curr_plate(); - bool only_has_gcode_need_preview = false; - bool current_has_print_instances = current_plate->has_printable_instances(); - if (current_plate->is_slice_result_valid() && this->model.objects.empty() && !current_has_print_instances) - only_has_gcode_need_preview = true; + // see: Plater::priv::object_list_changed() + // FIXME: it may be better to have a single function making this check and let it be called wherever needed + bool export_in_progress = this->background_process.is_export_scheduled(); + ObjectFilamentResults object_results; + bool model_fits = this->view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; + model_fits = model_fits && object_results.filaments.empty(); + // BBS: add partplate logic + PartPlate* current_plate = this->partplate_list.get_curr_plate(); + bool only_has_gcode_need_preview = false; + bool current_has_print_instances = current_plate->has_printable_instances(); + if (current_plate->is_slice_result_valid() && this->model.objects.empty() && !current_has_print_instances) + only_has_gcode_need_preview = true; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; + bool mixed_broken = sidebar->has_broken_mixed_filament(); - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) - { - //if already running in background, not relice here - //BBS: add more judge for slicing - if (!this->background_process.running() && !this->m_is_slicing) - { - this->m_slice_all = false; - this->q->reslice(); - } - else { - //reset current plate to the slicing plate - int plate_index = this->background_process.get_current_plate()->get_index(); - this->partplate_list.select_plate(plate_index); - } - } - else if (only_has_gcode_need_preview) - { + BOOST_LOG_TRIVIAL(info) + << __FUNCTION__ + << boost::format( + ": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%, mixed_broken %5%") % + no_slice % export_in_progress % model_fits % m_is_slicing % mixed_broken; + + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances && !mixed_broken) { + // if already running in background, not relice here + // BBS: add more judge for slicing + if (!this->background_process.running() && !this->m_is_slicing) { this->m_slice_all = false; this->q->reslice(); + } else { + // reset current plate to the slicing plate + int plate_index = this->background_process.get_current_plate()->get_index(); + this->partplate_list.select_plate(plate_index); } - //BBS: process empty plate, reset previous toolpath + } else if (only_has_gcode_need_preview) { + this->m_slice_all = false; + this->q->reslice(); + } + // BBS: process empty plate, reset previous toolpath + else { + // if (!this->m_slice_all) + if (!current_has_print_instances) + reset_gcode_toolpaths(); + // this->q->refresh_print(); + if (!preview->get_canvas3d()->is_initialized()) { + preview->get_canvas3d()->render(true); + } + } + // TODO: turn off this switch currently + /*auto canvas_w = float(preview->get_canvas3d()->get_canvas_size().get_width()); + auto canvas_h = float(preview->get_canvas3d()->get_canvas_size().get_height()); + Point screen_center(canvas_w/2, canvas_h/2); + auto center_point = preview->get_canvas3d()->_mouse_to_3d(screen_center); + center_point(2) = 0.f; + if (!current_plate->contains(center_point)) + this->partplate_list.select_plate_view();*/ + + // keeps current gcode preview, if any + if (this->m_slice_all) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": slicing all, just reload shells"); + this->update_fff_scene_only_shells(); + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": single slice, reload print"); + if (model_fits) + this->preview->reload_print(); // TODO else - { - //if (!this->m_slice_all) - if (!current_has_print_instances) - reset_gcode_toolpaths(); - //this->q->refresh_print(); - if (!preview->get_canvas3d()->is_initialized()) - { - preview->get_canvas3d()->render(true); - } - } - //TODO: turn off this switch currently - /*auto canvas_w = float(preview->get_canvas3d()->get_canvas_size().get_width()); - auto canvas_h = float(preview->get_canvas3d()->get_canvas_size().get_height()); - Point screen_center(canvas_w/2, canvas_h/2); - auto center_point = preview->get_canvas3d()->_mouse_to_3d(screen_center); - center_point(2) = 0.f; - if (!current_plate->contains(center_point)) - this->partplate_list.select_plate_view();*/ - - // keeps current gcode preview, if any - if (this->m_slice_all) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": slicing all, just reload shells"); this->update_fff_scene_only_shells(); - } - else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": single slice, reload print"); - if (model_fits) - this->preview->reload_print(); // TODO - else - this->update_fff_scene_only_shells(); - } + } - preview->set_as_dirty(); - }; + preview->set_as_dirty(); + }; // Add sidebar and toolbar collapse logic if (panel == view3D || panel == preview) { @@ -10291,8 +11957,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) } } - if (current_panel == panel) - { + if (current_panel == panel) { if (panel == view3D) { if (view3D->is_reload_delayed()) { // Delayed loading of the 3D scene when caller requests the already active tab. @@ -10307,8 +11972,8 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) if (notification_manager != nullptr) notification_manager->set_in_preview(false); } - //BBS: add slice logic when switch to preview page - //BBS: add only gcode mode + // BBS: add slice logic when switch to preview page + // BBS: add only gcode mode if (!q->only_gcode_mode() && (current_panel == preview) && (wxGetApp().is_editor())) { do_reslice(); } @@ -10316,17 +11981,17 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) return; } - //BBS: wish to reset all plates stats item selected state when back to View3D Tab + // BBS: wish to reset all plates stats item selected state when back to View3D Tab preview->get_canvas3d()->reset_select_plate_toolbar_selection(); wxPanel* old_panel = current_panel; -//#if BBL_HAS_FIRST_PAGE + // #if BBL_HAS_FIRST_PAGE if (!old_panel) { // Wayland may report the first canvas as not yet shown while the frame is still mapping. // Keep the panel switch anyway so handlers are bound and the first paint can initialize GL later. panel->Show(); } -//#endif + // #endif current_panel = panel; // to reduce flickering when changing view, first set as visible the new current panel @@ -10361,8 +12026,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) } if (current_panel == view3D || current_panel == preview) { cam.load_camera_view(view3D->get_canvas3d()->get_camera()); - } - else if (current_panel == assemble_view) { + } else if (current_panel == assemble_view) { cam.load_camera_view(assemble_view->get_canvas3d()->get_camera()); } } @@ -10373,12 +12037,12 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) else if (old_panel == assemble_view) { assemble_view->get_canvas3d()->unbind_event_handlers(); - GLCanvas3D* assemble_canvas = assemble_view->get_canvas3d(); + GLCanvas3D* assemble_canvas = assemble_view->get_canvas3d(); Selection::IndicesList select_idxs = assemble_canvas->get_selection().get_volume_idxs(); - Selection& view3d_selection = view3D->get_canvas3d()->get_selection(); + Selection& view3d_selection = view3D->get_canvas3d()->get_selection(); view3d_selection.clear(); for (unsigned int idx : select_idxs) { - auto v = assemble_canvas->get_selection().get_volume(idx); + auto v = assemble_canvas->get_selection().get_volume(idx); auto real_idx = view3d_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), v->volume_idx()); if (real_idx >= 0) { view3d_selection.add(real_idx, false); @@ -10403,11 +12067,10 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size view3D->get_canvas3d()->reset_old_size(); // BBS - //view_toolbar.select_item("3D"); + // view_toolbar.select_item("3D"); if (notification_manager != nullptr) notification_manager->set_in_preview(false); - } - else if (current_panel == preview) { + } else if (current_panel == preview) { q->invalid_all_plate_thumbnails(); if (old_panel == view3D) view3D->get_canvas3d()->unbind_event_handlers(); @@ -10450,28 +12113,27 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size preview->get_canvas3d()->reset_old_size(); // BBS - //view_toolbar.select_item("Preview"); + // view_toolbar.select_item("Preview"); if (notification_manager != nullptr) notification_manager->set_in_preview(true); - } - else if (current_panel == assemble_view) { + } else if (current_panel == assemble_view) { if (old_panel == view3D) { view3D->get_canvas3d()->unbind_event_handlers(); - } - else if (old_panel == preview) + } else if (old_panel == preview) preview->get_canvas3d()->unbind_event_handlers(); assemble_view->get_canvas3d()->bind_event_handlers(); assemble_view->reload_scene(true); if (old_panel == view3D) { - GLCanvas3D* view3D_canvas = view3D->get_canvas3d(); + GLCanvas3D* view3D_canvas = view3D->get_canvas3d(); Selection::IndicesList select_idxs = view3D_canvas->get_selection().get_volume_idxs(); - Selection& assemble_selection = assemble_view->get_canvas3d()->get_selection(); + Selection& assemble_selection = assemble_view->get_canvas3d()->get_selection(); assemble_selection.clear(); for (unsigned int idx : select_idxs) { auto v = view3D_canvas->get_selection().get_volume(idx); - auto real_idx = assemble_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), v->volume_idx()); + auto real_idx = assemble_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), + v->volume_idx()); if (real_idx >= 0) { assemble_selection.add(real_idx, false); } @@ -10481,12 +12143,12 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // BBS set default view and zoom if (first_enter_assemble) { wxGetApp().plater()->get_camera().requires_zoom_to_volumes = true; - first_enter_assemble = false; + first_enter_assemble = false; } assemble_view->set_as_dirty(); // BBS - //view_toolbar.select_item("Assemble"); + // view_toolbar.select_item("Assemble"); } current_panel->SetFocusFromKbd(); @@ -10495,33 +12157,32 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) } // BBS -void Plater::priv::on_combobox_select(wxCommandEvent &evt) +void Plater::priv::on_combobox_select(wxCommandEvent& evt) { PlaterPresetComboBox* preset_combo_box = dynamic_cast(evt.GetEventObject()); if (preset_combo_box) { this->on_select_preset(evt); sidebar->update_printer_thumbnail(); - } - else { + } else { this->on_select_bed_type(evt); } } -void Plater::priv::on_select_bed_type(wxCommandEvent &evt) +void Plater::priv::on_select_bed_type(wxCommandEvent& evt) { - ComboBox* combo = static_cast(evt.GetEventObject()); - auto select_bed_type = sidebar->get_cur_select_bed_type(); - std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[(int)select_bed_type - 1]; + ComboBox* combo = static_cast(evt.GetEventObject()); + auto select_bed_type = sidebar->get_cur_select_bed_type(); + std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[(int) select_bed_type - 1]; - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; if (keys_map) { BedType new_bed_type = btCount; for (auto item : *keys_map) { if (item.first == bed_type_name) { - new_bed_type = (BedType)item.second; + new_bed_type = (BedType) item.second; break; } } @@ -10539,10 +12200,10 @@ void Plater::priv::on_select_bed_type(wxCommandEvent &evt) // update app_config AppConfig* app_config = wxGetApp().app_config; app_config->set("curr_bed_type", std::to_string(int(new_bed_type))); - app_config->set_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), - "curr_bed_type", std::to_string(int(new_bed_type))); + app_config->set_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), "curr_bed_type", + std::to_string(int(new_bed_type))); - //update slice status + // update slice status auto plate_list = partplate_list.get_plate_list(); for (auto plate : plate_list) { if (plate->get_bed_type() == btDefault) { @@ -10558,7 +12219,7 @@ void Plater::priv::on_select_bed_type(wxCommandEvent &evt) } } -void Plater::priv::on_select_preset(wxCommandEvent &evt) +void Plater::priv::on_select_preset(wxCommandEvent& evt) { PlaterPresetComboBox* combo = static_cast(evt.GetEventObject()); Preset::Type preset_type = combo->get_type(); @@ -10578,11 +12239,11 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) // BBS:Save the plate parameters before switching PartPlateList& old_plate_list = this->partplate_list; - PartPlate* old_plate = old_plate_list.get_selected_plate(); - Vec3d old_plate_pos = old_plate->get_center_origin(); + PartPlate* old_plate = old_plate_list.get_selected_plate(); + Vec3d old_plate_pos = old_plate->get_center_origin(); // BBS: Save the model in the current platelist - std::vector > plate_object; + std::vector> plate_object; for (size_t i = 0; i < old_plate_list.get_plate_count(); ++i) { PartPlate* plate = old_plate_list.get_plate(i); std::vector obj_idxs; @@ -10610,8 +12271,8 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) preset_name = Preset::remove_suffix_modified(wx_stored_name.ToUTF8().data()); } else { wxString wx_name = combo->GetString(selection); - preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, - Preset::remove_suffix_modified(wx_name.ToUTF8().data())); + preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, + Preset::remove_suffix_modified(wx_name.ToUTF8().data())); } if (preset_type == Preset::TYPE_FILAMENT) { @@ -10624,7 +12285,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) sidebar->auto_calc_flushing_volumes(idx); } auto select_flag = combo->GetFlag(selection); - combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); + combo->ShowBadge(select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS); q->on_filament_change(idx); } bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); @@ -10632,11 +12293,10 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) if (preset_type == Preset::TYPE_FILAMENT && sidebar->is_multifilament()) { // Only update the plater UI for the 2nd and other filaments. combo->update(); - } - else if (select_preset) { + } else if (select_preset) { if (preset_type == Preset::TYPE_PRINTER) { PhysicalPrinterCollection& physical_printers = wxGetApp().preset_bundle->physical_printers; - if(combo->is_selected_physical_printer()) + if (combo->is_selected_physical_printer()) preset_name = physical_printers.get_selected_printer_preset_name(); else physical_printers.unselect_printer(); @@ -10648,7 +12308,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) dlg.ShowModal(); } preset->is_visible = true; // force visible - preset_name = preset->name; + preset_name = preset->name; } std::string old_preset_name = wxGetApp().preset_bundle->printers.get_edited_preset().name; @@ -10659,24 +12319,24 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) q->on_config_change(wxGetApp().preset_bundle->full_config()); }); - if (old_preset_name != preset_name && wxGetApp().app_config->get("auto_calculate_flush") == "all") { wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(-1); } // sync extruder info when select multi_extruder preset - if (Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager()) { - MachineObject *obj = dev->get_selected_machine(); + if (Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager()) { + MachineObject* obj = dev->get_selected_machine(); if (obj && obj->is_multi_extruders()) { - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - Preset& cur_preset = preset_bundle->printers.get_edited_preset(); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + Preset& cur_preset = preset_bundle->printers.get_edited_preset(); if (cur_preset.get_printer_type(preset_bundle) == obj->get_show_printer_type()) { double preset_nozzle_diameter = cur_preset.config.option("nozzle_diameter")->values[0]; - bool same_nozzle_diameter = true; + bool same_nozzle_diameter = true; const auto& extruders = obj->GetExtderSystem()->GetExtruders(); - for (const DevExtder &extruder : extruders) { - if (!obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(extruder.GetExtId(), float(preset_nozzle_diameter))) { + for (const DevExtder& extruder : extruders) { + if (!obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(extruder.GetExtId(), + float(preset_nozzle_diameter))) { same_nozzle_diameter = false; } } @@ -10706,17 +12366,17 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) // update plater with new config q->on_config_change(wxGetApp().preset_bundle->full_config()); if (preset_type == Preset::TYPE_PRINTER) { - /* Settings list can be changed after printer preset changing, so - * update all settings items for all item had it. - * Furthermore, Layers editing is implemented only for FFF printers - * and for SLA presets they should be deleted - */ + /* Settings list can be changed after printer preset changing, so + * update all settings items for all item had it. + * Furthermore, Layers editing is implemented only for FFF printers + * and for SLA presets they should be deleted + */ wxGetApp().obj_list()->update_object_list_by_printer_technology(); // BBS:Model reset by plate center PartPlateList& cur_plate_list = this->partplate_list; - PartPlate* cur_plate = cur_plate_list.get_curr_plate(); - Vec3d cur_plate_pos = cur_plate->get_center_origin(); + PartPlate* cur_plate = cur_plate_list.get_curr_plate(); + Vec3d cur_plate_pos = cur_plate->get_center_origin(); if (old_plate_pos.x() != cur_plate_pos.x() || old_plate_pos.y() != cur_plate_pos.y()) { for (int i = 0; i < plate_object.size(); ++i) { @@ -10727,16 +12387,15 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) view3D->deselect_all(); } -#if 0 // do not toggle auto calc when change printer - // update flush matrix +#if 0 // do not toggle auto calc when change printer + // update flush matrix size_t filament_size = wxGetApp().plater()->get_extruder_colors_from_plater_config().size(); for (size_t idx = 0; idx < filament_size; ++idx) wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(idx); #endif // Show shared profiles notification for the newly selected printer - if (wxGetApp().app_config->get_bool("show_shared_profiles_notification")) - { + if (wxGetApp().app_config->get_bool("show_shared_profiles_notification")) { std::string printer_name = wxGetApp().preset_bundle->printers.get_selected_preset_base().name; std::string encoded_name = Http::url_encode(printer_name); @@ -10771,27 +12430,29 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) // update slice state and set bedtype default for 3rd-party printer auto plate_list = partplate_list.get_plate_list(); for (auto plate : plate_list) { - plate->update_slice_result_valid_state(false); + plate->update_slice_result_valid_state(false); } } -void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) +void Plater::priv::on_slicing_update(SlicingStatusEvent& evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % evt.status.text; - //BBS: add slice project logic + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % + evt.status.text; + // BBS: add slice project logic std::string title_text = _u8L("Slicing"); - evt.status.text = title_text + evt.status.text; + evt.status.text = title_text + evt.status.text; if (evt.status.percent >= 0) { - if (!m_worker.is_idle()) { + if (!m_worker.is_idle()) { // Avoid a race condition return; } - notification_manager->set_slicing_progress_percentage(evt.status.text, (float)evt.status.percent / 100.0f); + notification_manager->set_slicing_progress_percentage(evt.status.text, (float) evt.status.percent / 100.0f); // update slicing percent PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); - //slicing parallel, only update if percent is greater than before + // slicing parallel, only update if percent is greater than before if (evt.status.percent > plate_list.get_curr_plate()->get_slicing_percent()) plate_list.get_curr_plate()->update_slicing_percent(evt.status.percent); } @@ -10799,9 +12460,9 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) if (evt.status.flags & (PrintBase::SlicingStatus::RELOAD_SCENE | PrintBase::SlicingStatus::RELOAD_SLA_SUPPORT_POINTS)) { switch (this->printer_technology) { case ptFFF: - //BBS: add slice project logic, only display shells at the beginning + // BBS: add slice project logic, only display shells at the beginning if (!m_slice_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1))) - //this->update_fff_scene(); + // this->update_fff_scene(); this->update_fff_scene_only_shells(); break; case ptSLA: @@ -10818,28 +12479,29 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) this->preview->reload_print(); } - if (evt.status.flags & (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { + if (evt.status.flags & + (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { // Update notification center with warnings of object_id and its warning_step. ObjectID object_id = evt.status.warning_object_id; - int warning_step = evt.status.warning_step; + int warning_step = evt.status.warning_step; PrintStateBase::StateWithWarnings state; - ModelObject const * model_object = nullptr; + ModelObject const* model_object = nullptr; - //BBS: add partplate related logic, use the print in background process + // BBS: add partplate related logic, use the print in background process if (evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) { state = this->printer_technology == ptFFF ? - this->background_process.m_fff_print->step_state_with_warnings(static_cast(warning_step)) : - this->background_process.m_sla_print->step_state_with_warnings(static_cast(warning_step)); + this->background_process.m_fff_print->step_state_with_warnings(static_cast(warning_step)) : + this->background_process.m_sla_print->step_state_with_warnings(static_cast(warning_step)); } else if (this->printer_technology == ptFFF) { - const PrintObject *print_object = this->background_process.m_fff_print->get_object(object_id); + const PrintObject* print_object = this->background_process.m_fff_print->get_object(object_id); if (print_object) { - state = print_object->step_state_with_warnings(static_cast(warning_step)); + state = print_object->step_state_with_warnings(static_cast(warning_step)); model_object = print_object->model_object(); } } else { - const SLAPrintObject *print_object = this->background_process.m_sla_print->get_object(object_id); + const SLAPrintObject* print_object = this->background_process.m_sla_print->get_object(object_id); if (print_object) { - state = print_object->step_state_with_warnings(static_cast(warning_step)); + state = print_object->step_state_with_warnings(static_cast(warning_step)); model_object = print_object->model_object(); } } @@ -10847,10 +12509,12 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) for (auto const& warning : state.warnings) { if (warning.current) { NotificationManager::NotificationLevel notif_level = NotificationManager::NotificationLevel::WarningNotificationLevel; - if (evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingReplaceInitEmptyLayers || evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingEmptyGcodeLayers) { + if (evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingReplaceInitEmptyLayers || + evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingEmptyGcodeLayers) { notif_level = NotificationManager::NotificationLevel::SeriousWarningNotificationLevel; } - notification_manager->push_slicing_warning_notification(warning.message, false, model_object, object_id, warning_step, warning.message_id, notif_level); + notification_manager->push_slicing_warning_notification(warning.message, false, model_object, object_id, warning_step, + warning.message_id, notif_level); add_warning(warning, object_id.id); } } @@ -10858,12 +12522,13 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("exit."); } -void Plater::priv::on_slicing_completed(wxCommandEvent & evt) +void Plater::priv::on_slicing_completed(wxCommandEvent& evt) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, string %2%") % evt.GetEventType() % evt.GetString(); - //BBS: add slice project logic + // BBS: add slice project logic if (m_slice_all && (m_cur_slice_plate < (partplate_list.get_plate_count() - 1))) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("slicing all, finished plate %1%, will continue next.")%m_cur_slice_plate; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ + << boost::format("slicing all, finished plate %1%, will continue next.") % m_cur_slice_plate; return; } @@ -10871,11 +12536,10 @@ void Plater::priv::on_slicing_completed(wxCommandEvent & evt) delayed_scene_refresh = true; else { if (this->printer_technology == ptFFF) { - //BBS: only reload shells + // BBS: only reload shells this->update_fff_scene_only_shells(false); - //this->update_fff_scene(); - } - else + // this->update_fff_scene(); + } else this->update_sla_scene(); } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("exit."); @@ -10905,8 +12569,8 @@ void Plater::priv::on_slicing_began() clear_warnings(); notification_manager->close_notification_of_type(NotificationType::SignDetected); notification_manager->close_notification_of_type(NotificationType::ExportFinished); - bool is_first_plate = m_cur_slice_plate == 0; - bool slice_all = q->m_only_gcode ? m_slice_all_only_has_gcode : m_slice_all; + bool is_first_plate = m_cur_slice_plate == 0; + bool slice_all = q->m_only_gcode ? m_slice_all_only_has_gcode : m_slice_all; bool need_change_dailytips = !(slice_all && !is_first_plate); notification_manager->set_slicing_progress_began(); notification_manager->update_slicing_notif_dailytips(need_change_dailytips); @@ -10915,8 +12579,7 @@ void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, s { for (auto& it : current_warnings) { if (warning.message_id == it.first.message_id) { - if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message)) - { + if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message)) { if (warning.message_id != 0) it.first.message = warning.message; return; @@ -10925,7 +12588,7 @@ void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, s } current_warnings.emplace_back(std::pair(warning, oid)); } -void Plater::priv::actualize_slicing_warnings(const PrintBase &print) +void Plater::priv::actualize_slicing_warnings(const PrintBase& print) { std::vector ids = print.print_object_ids(); if (ids.empty()) { @@ -10940,8 +12603,7 @@ void Plater::priv::actualize_slicing_warnings(const PrintBase &print) void Plater::priv::actualize_object_warnings(const PrintBase& print) { std::vector ids; - for (const ModelObject* object : print.model().objects ) - { + for (const ModelObject* object : print.model().objects) { ids.push_back(object->id()); } std::sort(ids.begin(), ids.end()); @@ -10965,31 +12627,29 @@ bool Plater::priv::warnings_dialog() else text += it.first.message; } - //text += "\n\nDo you still wish to export?"; + // text += "\n\nDo you still wish to export?"; MessageDialog msg_window(this->q, from_u8(text), _L("warnings"), wxOK); - const auto res = msg_window.ShowModal(); + const auto res = msg_window.ShowModal(); return res == wxID_OK; - } -//BBS: add project slice logic -void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) +// BBS: add project slice logic +void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, m_ignore_event %1%, status %2%")%m_ignore_event %evt.status(); - //BBS:ignore cancel event for some special case - if (m_ignore_event) - { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, m_ignore_event %1%, status %2%") % m_ignore_event % evt.status(); + // BBS:ignore cancel event for some special case + if (m_ignore_event) { m_ignore_event = false; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": ignore this event %1%") % evt.status(); return; } - //BBS: add project slice logic + // BBS: add project slice logic bool is_finished = !m_slice_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1)); - //BBS: slice .gcode.3mf file related logic, assign is_finished again + // BBS: slice .gcode.3mf file related logic, assign is_finished again bool only_has_gcode_need_preview = false; - auto plate_list = this->partplate_list.get_plate_list(); - bool has_print_instances = false; + auto plate_list = this->partplate_list.get_plate_list(); + bool has_print_instances = false; for (auto plate : plate_list) has_print_instances = has_print_instances || plate->has_printable_instances(); if (this->model.objects.empty() && !has_print_instances) @@ -11022,25 +12682,26 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) notification_manager->set_slicing_progress_hidden(); } } else { - std::vector ptrs; - for (auto oid : message.second) - { - const PrintObject *print_object = this->background_process.m_fff_print->get_object(ObjectID(oid)); - if (print_object) { ptrs.push_back(print_object->model_object()); } + std::vector ptrs; + for (auto oid : message.second) { + const PrintObject* print_object = this->background_process.m_fff_print->get_object(ObjectID(oid)); + if (print_object) { + ptrs.push_back(print_object->model_object()); + } } notification_manager->push_slicing_error_notification(message.first, ptrs); } - if (evt.invalidate_plater()) - { + if (evt.invalidate_plater()) { // BBS #if 0 const wxString invalid_str = _L("Invalid data"); for (auto btn : { ActionButtonType::abReslice, ActionButtonType::abSendGCode, ActionButtonType::abExport }) sidebar->set_btn_label(btn, invalid_str); #endif - process_completed_with_error = partplate_list.get_curr_plate_index();; + process_completed_with_error = partplate_list.get_curr_plate_index(); + ; } - has_error = true; + has_error = true; is_finished = true; } if (evt.cancelled()) { @@ -11049,11 +12710,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) is_finished = true; } - //BBS: set the current plater's slice result to valid + // BBS: set the current plater's slice result to valid if (!this->background_process.empty()) this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); - //BBS: update the action button according to the current plate's status + // BBS: update the action button according to the current plate's status bool ready_to_slice = !this->partplate_list.get_curr_plate()->is_slice_result_valid(); // BBS @@ -11064,8 +12725,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) // This updates the "Slice now", "Export G-code", "Arrange" buttons status. // Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables // the "Slice now" and "Export G-code" buttons based on their "out of bed" status. - //BBS: remove this update here, will be updated in update_fff_scene later - //this->object_list_changed(); + // BBS: remove this update here, will be updated in update_fff_scene later + // this->object_list_changed(); // refresh preview if (view3D->is_dragging()) // updating scene now would interfere with the gizmo dragging @@ -11074,28 +12735,27 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) if (this->printer_technology == ptFFF) { if (is_finished) this->update_fff_scene(); - } - else + } else this->update_sla_scene(); } - //BBS: add slice&&print status update logic + // BBS: add slice&&print status update logic if (evt.cancelled()) { /*if (wxGetApp().get_mode() == comSimple) sidebar->set_btn_label(ActionButtonType::abReslice, "Slice now"); show_action_buttons(true);*/ ready_to_slice = true; - //this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true, true); + // this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true, true); - //BBS + // BBS if (m_is_publishing) { m_publish_dlg->cancel(); } } else { - if((ready_to_slice) || (wxGetApp().get_mode() == comSimple)) { - //this means the current plate is not the slicing plate - //show_action_buttons(ready_to_slice); - //this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); + if ((ready_to_slice) || (wxGetApp().get_mode() == comSimple)) { + // this means the current plate is not the slicing plate + // show_action_buttons(ready_to_slice); + // this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); } if (exporting_status != ExportingStatus::NOT_EXPORTING && !has_error) { notification_manager->stop_delayed_notifications_of_type(NotificationType::ExportOngoing); @@ -11103,14 +12763,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) } // If writing to removable drive was scheduled, show notification with eject button if (exporting_status == ExportingStatus::EXPORTING_TO_REMOVABLE && !has_error) { - //show_action_buttons(ready_to_slice); + // show_action_buttons(ready_to_slice); this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); - notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, - // Don't offer the "Eject" button on ChromeOS, the Linux side has no control over it. - platform_flavor() != PlatformFlavor::LinuxOnChromium); + notification_manager + ->push_exporting_finished_notification(last_output_path, last_output_dir_path, + // Don't offer the "Eject" button on ChromeOS, the Linux side has no control over it. + platform_flavor() != PlatformFlavor::LinuxOnChromium); wxGetApp().removable_drive_manager()->set_exporting_finished(true); - }else - if (exporting_status == ExportingStatus::EXPORTING_TO_LOCAL && !has_error) + } else if (exporting_status == ExportingStatus::EXPORTING_TO_LOCAL && !has_error) notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, false); // BBS, Generate calibration thumbnail for current plate @@ -11120,20 +12780,19 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) preview->reload_print(); ThumbnailData* calibration_data = &partplate_list.get_curr_plate()->cali_thumbnail_data; const ThumbnailsParams calibration_params = { {}, false, true, true, true, partplate_list.get_curr_plate_index() }; - generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, calibration_params); - preview->get_canvas3d()->reset_gcode_toolpaths();*/ + generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, + calibration_params); preview->get_canvas3d()->reset_gcode_toolpaths();*/ // generate bbox data PlateBBoxData* plate_bbox_data = &partplate_list.get_curr_plate()->cali_bboxes_data; - *plate_bbox_data = generate_first_layer_bbox(); + *plate_bbox_data = generate_first_layer_bbox(); } } exporting_status = ExportingStatus::NOT_EXPORTING; - // BBS stop publishing if error occur - //if (m_is_publishing) { + // if (m_is_publishing) { // GCodeProcessorResult *gcode_result = background_process.get_current_gcode_result(); // m_publish_dlg->UpdateStatus(_L("Error occurred during slicing"), -1, false); // // if toolpath is outside @@ -11142,9 +12801,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) // } //} - - if (is_finished) - { + if (is_finished) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":finished, reload print soon"); m_is_slicing = false; this->preview->reload_print(false); @@ -11160,10 +12817,9 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) } } q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); - } - else - { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(":slicing all, plate %1% finished, start next slice...") % m_cur_slice_plate; m_cur_slice_plate++; q->Freeze(); @@ -11171,10 +12827,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) partplate_list.select_plate_view(); int ret = q->start_next_slice(); if (ret) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% can not be sliced, will stop")%m_cur_slice_plate; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(":slicing all, plate %1% can not be sliced, will stop") % m_cur_slice_plate; m_is_slicing = false; } - //not the last plate + // not the last plate update_fff_scene_only_shells(); q->Thaw(); if (m_is_publishing) { @@ -11196,13 +12853,13 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) void Plater::priv::on_action_add(SimpleEvent&) { if (q != nullptr) { - //q->add_model(); - //BBS open file in toolbar add + // q->add_model(); + // BBS open file in toolbar add q->add_file(); } } -//BBS: add plate from toolbar +// BBS: add plate from toolbar void Plater::priv::on_action_add_plate(SimpleEvent&) { if (q != nullptr) { @@ -11213,22 +12870,22 @@ void Plater::priv::on_action_add_plate(SimpleEvent&) update(); // BBS set default view - //q->get_camera().select_view("topfront"); + // q->get_camera().select_view("topfront"); q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; } } -//BBS: remove plate from toolbar +// BBS: remove plate from toolbar void Plater::priv::on_action_del_plate(SimpleEvent&) { if (q != nullptr) { q->delete_plate(); - //q->get_camera().select_view("topfront"); - //q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + // q->get_camera().select_view("topfront"); + // q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; } } -//BBS: GUI refactor: GLToolbar +// BBS: GUI refactor: GLToolbar void Plater::priv::on_action_open_project(SimpleEvent&) { if (q != nullptr) { @@ -11236,16 +12893,16 @@ void Plater::priv::on_action_open_project(SimpleEvent&) } } -//BBS: GUI refactor: slice plate +// BBS: GUI refactor: slice plate void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; - //BBS update extruder params and speed table before slicing + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n"; + // BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); - auto& print = q->get_partplate_list().get_current_fff_print(); - auto print_config = print.config(); - int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); @@ -11255,33 +12912,33 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) } } -//BBS: GUI refactor: slice all +// BBS: GUI refactor: slice all void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; - //BBS update extruder params and speed table before slicing + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n"; + // BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); - auto& print = q->get_partplate_list().get_current_fff_print(); - auto print_config = print.config(); - int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - m_slice_all = true; + m_slice_all = true; m_slice_all_only_has_gcode = true; - m_cur_slice_plate = 0; - //select plate + m_cur_slice_plate = 0; + // select plate q->select_plate(m_cur_slice_plate); q->reslice(); if (!m_is_publishing) q->select_view_3D("Preview"); - //BBS: wish to select all plates stats item + // BBS: wish to select all plates stats item preview->get_canvas3d()->_update_select_plate_toolbar_stats_item(true); } } -void Plater::priv::on_action_publish(wxCommandEvent &event) +void Plater::priv::on_action_publish(wxCommandEvent& event) { if (q != nullptr) { if (event.GetInt() == EVT_PUBLISHING_START) { @@ -11311,7 +12968,7 @@ void Plater::priv::on_action_publish(wxCommandEvent &event) void Plater::priv::on_action_print_plate(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n"; } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; @@ -11346,7 +13003,7 @@ void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n"; } - //BBS + // BBS open_machine_select_dialog(0, PrintFromType::FROM_SDCARD_VIEW); } @@ -11365,12 +13022,11 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) return; } const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel); - sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; + sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; update_sidebar(); - int old_sel = e.GetOldSelection(); - const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); - const bool use_native_device_tab = wxGetApp().preset_bundle && - (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); + int old_sel = e.GetOldSelection(); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + const bool use_native_device_tab = wxGetApp().preset_bundle && (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); if (use_native_device_tab && new_name == TAB_ID_MONITOR) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. @@ -11387,12 +13043,12 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) // while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds // TAB_ID_MONITOR itself. const bool selecting_web_device_tab = main_frame->m_printer_view && - main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; if (selecting_web_device_tab) { // Use the selected discovered machine when the preset has no host. main_frame->load_printer_url(); } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) { - auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { // It's missing_connection page, reload so that we can replay the gif image @@ -11402,30 +13058,34 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } -int Plater::priv::update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path) +int Plater::priv::update_print_required_data(Slic3r::DynamicPrintConfig config, + Slic3r::Model model, + Slic3r::PlateDataPtrs plate_data_list, + std::string file_name, + std::string file_path) { - if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); + if (!m_select_machine_dlg) + m_select_machine_dlg = new SelectMachineDialog(q); return m_select_machine_dlg->update_print_required_data(config, model, plate_data_list, file_name, file_path); } void Plater::priv::on_action_send_to_printer(bool isall) { - if (!m_send_to_sdcard_dlg) m_send_to_sdcard_dlg = new SendToPrinterDialog(q); + if (!m_send_to_sdcard_dlg) + m_send_to_sdcard_dlg = new SendToPrinterDialog(q); if (isall) { m_send_to_sdcard_dlg->prepare(PLATE_ALL_IDX); - } - else { + } else { m_send_to_sdcard_dlg->prepare(partplate_list.get_curr_plate_index()); } - m_send_to_sdcard_dlg->ShowModal(); + m_send_to_sdcard_dlg->ShowModal(); } - -void Plater::priv::on_action_select_sliced_plate(wxCommandEvent &evt) +void Plater::priv::on_action_select_sliced_plate(wxCommandEvent& evt) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received select sliced plate event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received select sliced plate event\n"; } bool skip_zoom = evt.GetExtraLong() == 1; q->select_sliced_plate(evt.GetInt(), skip_zoom); @@ -11434,7 +13094,7 @@ void Plater::priv::on_action_select_sliced_plate(wxCommandEvent &evt) void Plater::priv::on_action_print_all(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print all event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print all event\n"; } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; @@ -11448,7 +13108,7 @@ void Plater::priv::on_action_print_all(SimpleEvent&) void Plater::priv::on_action_export_gcode(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n"; q->export_gcode(false); } } @@ -11456,7 +13116,7 @@ void Plater::priv::on_action_export_gcode(SimpleEvent&) void Plater::priv::on_action_send_gcode(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n"; q->send_gcode_legacy(); } } @@ -11464,12 +13124,12 @@ void Plater::priv::on_action_send_gcode(SimpleEvent&) void Plater::priv::on_action_export_sliced_file(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; q->export_gcode_3mf(); } } -void Plater::priv::on_action_export_all_sliced_file(SimpleEvent &) +void Plater::priv::on_action_export_all_sliced_file(SimpleEvent&) { if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export all sliced file event\n"; @@ -11479,10 +13139,10 @@ void Plater::priv::on_action_export_all_sliced_file(SimpleEvent &) void Plater::priv::on_action_export_to_sdcard(SimpleEvent&) { - if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; - q->send_to_printer(); - } + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; + q->send_to_printer(); + } } void Plater::priv::on_action_export_to_sdcard_all(SimpleEvent&) @@ -11493,16 +13153,16 @@ void Plater::priv::on_action_export_to_sdcard_all(SimpleEvent&) } } -//BBS: add plate select logic +// BBS: add plate select logic void Plater::priv::on_plate_selected(SimpleEvent&) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received plate selected event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received plate selected event\n"; sidebar->obj_list()->on_plate_selected(partplate_list.get_curr_plate_index()); } void Plater::priv::on_action_request_model_id(wxCommandEvent& evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received import model id event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received import model id event\n"; if (q != nullptr) { q->import_model_id(evt.GetString()); } @@ -11510,29 +13170,23 @@ void Plater::priv::on_action_request_model_id(wxCommandEvent& evt) void Plater::priv::on_action_download_project(wxCommandEvent& evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received download project event\n" ; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received download project event\n"; if (q != nullptr) { q->download_project(evt.GetString()); } } -//BBS: add slice button status update logic +// BBS: add slice button status update logic void Plater::priv::on_slice_button_status(bool enable) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": enable = "<update_slice_print_status(MainFrame::eEventObjectUpdate, enable); } -void Plater::priv::on_action_split_objects(SimpleEvent&) -{ - split_object(); -} +void Plater::priv::on_action_split_objects(SimpleEvent&) { split_object(); } -void Plater::priv::on_action_split_volumes(SimpleEvent&) -{ - split_volume(); -} +void Plater::priv::on_action_split_volumes(SimpleEvent&) { split_volume(); } void Plater::priv::on_object_select(SimpleEvent& evt) { @@ -11543,16 +13197,13 @@ void Plater::priv::on_object_select(SimpleEvent& evt) selection_changed(); } -//BBS: repair model through cgal -void Plater::priv::on_repair_model(wxCommandEvent &event) -{ - wxGetApp().obj_list()->fix_through_cgal(); -} +// BBS: repair model through cgal +void Plater::priv::on_repair_model(wxCommandEvent& event) { wxGetApp().obj_list()->fix_through_cgal(); } -void Plater::priv::on_filament_color_changed(wxCommandEvent &event) +void Plater::priv::on_filament_color_changed(wxCommandEvent& event) { - //q->update_all_plate_thumbnails(true); - //q->get_preview_canvas3D()->update_plate_thumbnails(); + // q->update_all_plate_thumbnails(true); + // q->get_preview_canvas3D()->update_plate_thumbnails(); int modify_id = event.GetInt(); auto& ams_multi_color_filment = wxGetApp().preset_bundle->ams_multi_color_filment; @@ -11562,19 +13213,22 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event) if (wxGetApp().app_config->get("auto_calculate_flush") != "disabled") { sidebar->auto_calc_flushing_volumes(modify_id); } + + // A mixed slot's colour is derived from its components, so recompute the swatches. + sidebar->update_mixed_filament_list(); } -void Plater::priv::install_network_plugin(wxCommandEvent &event) +void Plater::priv::install_network_plugin(wxCommandEvent& event) { wxGetApp().ShowDownNetPluginDlg(); return; } -void Plater::priv::update_plugin_when_launch(wxCommandEvent &event) +void Plater::priv::update_plugin_when_launch(wxCommandEvent& event) { std::string data_dir_str = data_dir(); boost::filesystem::path data_dir_path(data_dir_str); - auto cache_folder = data_dir_path / "ota"; + auto cache_folder = data_dir_path / "ota"; std::string changelog_file = cache_folder.string() + "/plugins/network_plugins.json"; UpdatePluginDialog dlg(wxGetApp().mainframe); @@ -11582,7 +13236,8 @@ void Plater::priv::update_plugin_when_launch(wxCommandEvent &event) auto result = dlg.ShowModal(); auto app_config = wxGetApp().app_config; - if (!app_config) return; + if (!app_config) + return; if (result == wxID_OK) { // Apply the downloaded update right away and hot-reload the plug-in, the same @@ -11593,32 +13248,36 @@ void Plater::priv::update_plugin_when_launch(wxCommandEvent &event) notification_manager->close_notification_of_type(NotificationType::BBLPluginUpdateAvailable); app_config->set("update_network_plugin", "false"); if (wxGetApp().hot_reload_network_plugin()) { - MessageDialog dlg_ok(wxGetApp().mainframe, _L("Network plug-in switched successfully."), _L("Success"), wxOK | wxICON_INFORMATION); + MessageDialog dlg_ok(wxGetApp().mainframe, _L("Network plug-in switched successfully."), _L("Success"), + wxOK | wxICON_INFORMATION); dlg_ok.ShowModal(); } else { - MessageDialog dlg_fail(wxGetApp().mainframe, _L("Failed to load network plug-in. Please restart the application."), _L("Restart Required"), wxOK | wxICON_WARNING); + MessageDialog dlg_fail(wxGetApp().mainframe, _L("Failed to load network plug-in. Please restart the application."), + _L("Restart Required"), wxOK | wxICON_WARNING); dlg_fail.ShowModal(); } } else { app_config->set("update_network_plugin", had_cache ? "true" : "false"); } - } - else if (result == wxID_NO) { + } else if (result == wxID_NO) { app_config->set("update_network_plugin", "false"); } } -void Plater::priv::show_install_plugin_hint(wxCommandEvent &event) +void Plater::priv::show_install_plugin_hint(wxCommandEvent& event) { - notification_manager->bbl_show_plugin_install_notification(into_u8(_L("The network plug-in was not detected. Network related features are unavailable."))); + notification_manager->bbl_show_plugin_install_notification( + into_u8(_L("The network plug-in was not detected. Network related features are unavailable."))); } -void Plater::priv::show_preview_only_hint(wxCommandEvent &event) +void Plater::priv::show_preview_only_hint(wxCommandEvent& event) { - notification_manager->bbl_show_preview_only_notification(into_u8(_L("Preview only mode:\nThe loaded file contains G-code only, cannot enter the Prepare page."))); + notification_manager->bbl_show_preview_only_notification( + into_u8(_L("Preview only mode:\nThe loaded file contains G-code only, cannot enter the Prepare page."))); } -void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) { +void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) +{ m_is_dark = wxSystemSettings::GetAppearance().IsDark(); if (view3D->get_canvas3d() && view3D->get_canvas3d()->is_initialized()) { view3D->get_canvas3d()->on_change_color_mode(m_is_dark); @@ -11629,22 +13288,25 @@ void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) { apply_color_mode(); } -void Plater::priv::on_change_color_mode(SimpleEvent& evt) { +void Plater::priv::on_change_color_mode(SimpleEvent& evt) +{ m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; sidebar->on_change_color_mode(m_is_dark); view3D->get_canvas3d()->on_change_color_mode(m_is_dark); preview->get_canvas3d()->on_change_color_mode(m_is_dark); assemble_view->get_canvas3d()->on_change_color_mode(m_is_dark); - if (m_send_to_sdcard_dlg) m_send_to_sdcard_dlg->on_change_color_mode(); + if (m_send_to_sdcard_dlg) + m_send_to_sdcard_dlg->on_change_color_mode(); apply_color_mode(); } void Plater::priv::apply_color_mode() { - const bool is_dark = wxGetApp().dark_mode(); - wxColour orca_color = wxColour(59, 68, 70);//wxColour(ColorRGBA::ORCA().r_uchar(), ColorRGBA::ORCA().g_uchar(), ColorRGBA::ORCA().b_uchar()); - orca_color = is_dark ? StateColor::darkModeColorFor(orca_color) : StateColor::lightModeColorFor(orca_color); + const bool is_dark = wxGetApp().dark_mode(); + wxColour orca_color = wxColour(59, 68, + 70); // wxColour(ColorRGBA::ORCA().r_uchar(), ColorRGBA::ORCA().g_uchar(), ColorRGBA::ORCA().b_uchar()); + orca_color = is_dark ? StateColor::darkModeColorFor(orca_color) : StateColor::lightModeColorFor(orca_color); wxColour sash_color = is_dark ? wxColour(38, 46, 48) : wxColour(206, 206, 206); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, sash_color); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); @@ -11652,7 +13314,8 @@ void Plater::priv::apply_color_mode() m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); } -static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { +static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) +{ int res_x = 0, res_y = 0; while (child != until_parent && child != nullptr) { @@ -11668,7 +13331,7 @@ static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x y = res_y; } -void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu *menu) +void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu* menu) { // BBS: GUI refactor: move sidebar to the left int x, y; @@ -11679,7 +13342,7 @@ void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu *menu) // specified (even though the position is sane). position = wxDefaultPosition; #endif - GLCanvas3D &canvas = *q->canvas3D(); + GLCanvas3D& canvas = *q->canvas3D(); canvas.apply_retina_scale(mouse_position); canvas.set_popup_menu_position(mouse_position); q->PopupMenu(menu, position); @@ -11692,22 +13355,19 @@ void Plater::priv::on_right_click(RBtnEvent& evt) wxMenu* menu = nullptr; - if (obj_idx == -1) { // no one or several object are selected - if (evt.data.second) { // right button was clicked on empty space + if (obj_idx == -1) { // no one or several object are selected + if (evt.data.second) { // right button was clicked on empty space if (!get_selection().is_empty()) // several objects are selected in 3DScene return; menu = menus.default_menu(); - } - else { + } else { if (current_panel == assemble_view) { menu = menus.assemble_multi_selection_menu(); - } - else { + } else { menu = menus.multi_selection_menu(); } } - } - else { + } else { // If in 3DScene is(are) selected volume(s), but right button was clicked on empty space if (evt.data.second) return; @@ -11721,24 +13381,24 @@ void Plater::priv::on_right_click(RBtnEvent& evt) else { const Selection& selection = get_selection(); // show "Object menu" for each one or several FullInstance instead of FullObject - const bool is_some_full_instances = selection.is_single_full_instance() || - selection.is_single_full_object() || + const bool is_some_full_instances = selection.is_single_full_instance() || selection.is_single_full_object() || selection.is_multiple_full_instance(); - const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); + const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); - //BBS get assemble view menu + // BBS get assemble view menu if (current_panel == assemble_view) { - menu = is_some_full_instances ? menus.assemble_object_menu() : - is_part ? menus.assemble_part_menu() : menus.assemble_multi_selection_menu(); + menu = is_some_full_instances ? menus.assemble_object_menu() : + is_part ? menus.assemble_part_menu() : + menus.assemble_multi_selection_menu(); } else { if (is_some_full_instances) menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); else if (is_part) { - const GLVolume* gl_volume = selection.get_first_volume(); - const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); - menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : - (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : - menus.part_menu(); + const GLVolume* gl_volume = selection.get_first_volume(); + const ModelVolume* model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); + menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : + (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); } else menu = menus.multi_selection_menu(); } @@ -11750,10 +13410,10 @@ void Plater::priv::on_right_click(RBtnEvent& evt) } } -//BBS: add part plate related logic +// BBS: add part plate related logic void Plater::priv::on_plate_right_click(RBtnPlateEvent& evt) { - wxMenu *menu = menus.plate_menu(); + wxMenu* menu = menus.plate_menu(); show_right_click_menu(evt.data.first, menu); } @@ -11762,10 +13422,7 @@ void Plater::priv::on_update_geometry(Vec3dsEvent<2>&) // TODO } -void Plater::priv::on_3dcanvas_mouse_dragging_started(SimpleEvent&) -{ - view3D->get_canvas3d()->reset_sequential_print_clearance(); -} +void Plater::priv::on_3dcanvas_mouse_dragging_started(SimpleEvent&) { view3D->get_canvas3d()->reset_sequential_print_clearance(); } // Update the scene from the background processing, // if the update message was received during mouse manipulation. @@ -11776,23 +13433,21 @@ void Plater::priv::on_3dcanvas_mouse_dragging_finished(SimpleEvent&) update_sla_scene(); } - //partplate_list.reload_all_objects(); + // partplate_list.reload_all_objects(); } -//BBS: add plate id for thumbnail generate param -void Plater::priv::generate_thumbnail(ThumbnailData & data, - unsigned int w, - unsigned int h, - const ThumbnailsParams &thumbnail_params, - Camera::EType camera_type, - Camera::ViewAngleType camera_view_angle_type, - bool for_picking, - bool ban_light) -{ - view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type, camera_view_angle_type, for_picking, ban_light); -} +// BBS: add plate id for thumbnail generate param +void Plater::priv::generate_thumbnail(ThumbnailData& data, + unsigned int w, + unsigned int h, + const ThumbnailsParams& thumbnail_params, + Camera::EType camera_type, + Camera::ViewAngleType camera_view_angle_type, + bool for_picking, + bool ban_light) +{ view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type, camera_view_angle_type, for_picking, ban_light); } -//BBS: add plate id for thumbnail generate param +// BBS: add plate id for thumbnail generate param ThumbnailsList Plater::priv::generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type) { ThumbnailsList thumbnails; @@ -11811,49 +13466,48 @@ PlateBBoxData Plater::priv::generate_first_layer_bbox() PlateBBoxData bboxdata; std::vector& id_bboxes = bboxdata.bbox_objs; BoundingBoxf bbox_all; - auto print = this->background_process.m_fff_print; - auto curr_plate = this->partplate_list.get_curr_plate(); - auto curr_plate_seq = curr_plate->get_real_print_seq(); - bboxdata.is_seq_print = (curr_plate_seq == PrintSequence::ByObject); - bboxdata.first_extruder = print->get_tool_ordering().first_extruder(); - bboxdata.bed_type = bed_type_to_gcode_string(print->config().curr_bed_type.value); + auto print = this->background_process.m_fff_print; + auto curr_plate = this->partplate_list.get_curr_plate(); + auto curr_plate_seq = curr_plate->get_real_print_seq(); + bboxdata.is_seq_print = (curr_plate_seq == PrintSequence::ByObject); + bboxdata.first_extruder = print->get_tool_ordering().first_extruder(); + bboxdata.bed_type = bed_type_to_gcode_string(print->config().curr_bed_type.value); bboxdata.first_layer_time = partplate_list.get_curr_plate()->get_slice_result()->initial_layer_time; // get nozzle diameter auto opt_nozzle_diameters = print->config().option("nozzle_diameter"); if (opt_nozzle_diameters != nullptr) bboxdata.nozzle_diameter = float(opt_nozzle_diameters->get_at(bboxdata.first_extruder)); - //PrintObjectPtrs objects; - //if (this->printer_technology == ptFFF) { - // objects = this->background_process.m_fff_print->objects().vector(); - //} - //else { - // objects = this->background_process.m_sla_print->objects(); - //} + // PrintObjectPtrs objects; + // if (this->printer_technology == ptFFF) { + // objects = this->background_process.m_fff_print->objects().vector(); + // } + // else { + // objects = this->background_process.m_sla_print->objects(); + // } auto objects = print->objects(); - auto orig = this->partplate_list.get_curr_plate()->get_origin(); - Vec2d orig2d = { orig[0], orig[1] }; + auto orig = this->partplate_list.get_curr_plate()->get_origin(); + Vec2d orig2d = {orig[0], orig[1]}; BBoxData data; - for (auto obj : objects) - { + for (auto obj : objects) { auto bb_scaled = obj->get_first_layer_bbox(data.area, data.layer_height, data.name); - auto bb = unscaled(bb_scaled); + auto bb = unscaled(bb_scaled); bb.min -= orig2d; bb.max -= orig2d; bbox_all.merge(bb); data.area *= (SCALING_FACTOR * SCALING_FACTOR); // unscale area - data.id = obj->id().id; - data.bbox = { bb.min.x(),bb.min.y(),bb.max.x(),bb.max.y() }; + data.id = obj->id().id; + data.bbox = {bb.min.x(), bb.min.y(), bb.max.x(), bb.max.y()}; id_bboxes.emplace_back(data); } // add wipe tower bounding box if (print->has_wipe_tower()) { - auto wt_corners = print->first_layer_wipe_tower_corners(); + auto wt_corners = print->first_layer_wipe_tower_corners(); // when loading gcode.3mf, wipe tower info may not be correct if (!wt_corners.empty()) { BoundingBox bb_scaled = {wt_corners[0], wt_corners[2]}; - auto bb = unscaled(bb_scaled); + auto bb = unscaled(bb_scaled); bb.min -= orig2d; bb.max -= orig2d; bbox_all.merge(bb); @@ -11864,7 +13518,7 @@ PlateBBoxData Plater::priv::generate_first_layer_bbox() } } - bboxdata.bbox_all = { bbox_all.min.x(),bbox_all.min.y(),bbox_all.max.x(),bbox_all.max.y() }; + bboxdata.bbox_all = {bbox_all.min.x(), bbox_all.min.y(), bbox_all.max.x(), bbox_all.max.y()}; return bboxdata; } @@ -11893,7 +13547,8 @@ wxString Plater::priv::get_export_gcode_filename(const wxString& extension, bool auto full_filename = m_project_folder / std::string((m_project_name + extension).mb_str(wxConvUTF8)); return from_path(full_filename); } else { - auto full_filename = m_project_folder / std::string((m_project_name + from_u8(plate_index_str) + extension).mb_str(wxConvUTF8)); + auto full_filename = m_project_folder / + std::string((m_project_name + from_u8(plate_index_str) + extension).mb_str(wxConvUTF8)); return from_path(full_filename); } } else { @@ -11904,30 +13559,26 @@ wxString Plater::priv::get_export_gcode_filename(const wxString& extension, bool } } else { if (only_filename) { - if(!model.objects.empty() && m_project_name == _L("Untitled")) + if (!model.objects.empty() && m_project_name == _L("Untitled")) return wxString(fs::path(model.objects.front()->name).replace_extension().c_str()) + from_u8(plate_index_str) + extension; if (export_all) return m_project_name + extension; else return m_project_name + from_u8(plate_index_str) + extension; - } - else + } else return ""; } } -wxString Plater::priv::get_project_name() -{ - return m_project_name; -} +wxString Plater::priv::get_project_name() { return m_project_name; } -//BBS +// BBS void Plater::priv::set_project_name(const wxString& project_name) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " project is:" << project_name; m_project_name = project_name; - //update topbar title + // update topbar title #ifdef __APPLE__ wxGetApp().mainframe->SetTitle(m_project_name); if (!m_project_name.IsEmpty()) @@ -11955,30 +13606,30 @@ void Plater::priv::update_title_dirty_status() #else wxGetApp().mainframe->SetTitle(title + " - OrcaSlicer"); wxGetApp().mainframe->topbar()->SetTitle(title); -#endif +#endif } void Plater::priv::set_project_filename(const wxString& filename) { boost::filesystem::path full_path = into_path(filename); - boost::filesystem::path ext = full_path.extension(); - //if (boost::iequals(ext.string(), ".amf")) { - // // Remove the first extension. - // full_path.replace_extension(""); - // // It may be ".zip.amf". - // if (boost::iequals(full_path.extension().string(), ".zip")) - // // Remove the 2nd extension. - // full_path.replace_extension(""); - //} else { - // // Remove just one extension. - // full_path.replace_extension(""); - //} + boost::filesystem::path ext = full_path.extension(); + // if (boost::iequals(ext.string(), ".amf")) { + // // Remove the first extension. + // full_path.replace_extension(""); + // // It may be ".zip.amf". + // if (boost::iequals(full_path.extension().string(), ".zip")) + // // Remove the 2nd extension. + // full_path.replace_extension(""); + // } else { + // // Remove just one extension. + // full_path.replace_extension(""); + // } full_path.replace_extension(""); m_project_folder = full_path.parent_path(); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " project folder is:" << m_project_folder.string(); - //BBS + // BBS wxString project_name = from_u8(full_path.filename().string()); set_project_name(project_name); // record filename for hint when open exported file/.gcode @@ -12010,14 +13661,14 @@ void Plater::priv::init_notification_manager() notification_manager->init_progress_indicator(); } -void Plater::priv::update_objects_position_when_select_preset(const std::function &select_prest) +void Plater::priv::update_objects_position_when_select_preset(const std::function& select_prest) { select_prest(); wxGetApp().obj_list()->update_object_list_by_printer_technology(); // Re-clamp wipe tower positions to new bed boundaries after preset change - PartPlateList &cur_plate_list = this->partplate_list; + PartPlateList& cur_plate_list = this->partplate_list; for (size_t plate_id = 0; plate_id < cur_plate_list.get_plate_list().size(); ++plate_id) { cur_plate_list.set_default_wipe_tower_pos_for_plate(plate_id); } @@ -12027,29 +13678,23 @@ void Plater::priv::update_objects_position_when_select_preset(const std::functio void Plater::orient() { - auto &w = get_ui_job_worker(); + auto& w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Orient")); replace_job(w, std::make_unique()); } } -//BBS: add job state related functions -void Plater::set_prepare_state(int state) -{ - p->m_job_prepare_state = state; -} +// BBS: add job state related functions +void Plater::set_prepare_state(int state) { p->m_job_prepare_state = state; } -int Plater::get_prepare_state() -{ - return p->m_job_prepare_state; -} +int Plater::get_prepare_state() { return p->m_job_prepare_state; } void Plater::get_print_job_data(PrintPrepareData* data) { if (data) { - data->plate_idx = p->m_print_job_data.plate_idx; - data->_3mf_path = p->m_print_job_data._3mf_path; + data->plate_idx = p->m_print_job_data.plate_idx; + data->_3mf_path = p->m_print_job_data._3mf_path; data->_3mf_config_path = p->m_print_job_data._3mf_config_path; } } @@ -12058,32 +13703,18 @@ void Plater::set_print_job_plate_idx(int plate_idx) { if (plate_idx == PLATE_CURRENT_IDX) { p->m_print_job_data.plate_idx = get_partplate_list().get_curr_plate_index(); - } - else { + } else { p->m_print_job_data.plate_idx = plate_idx; } } +int Plater::get_send_calibration_finished_event() { return EVT_SEND_CALIBRATION_FINISHED; } -int Plater::get_send_calibration_finished_event() -{ - return EVT_SEND_CALIBRATION_FINISHED; -} +int Plater::get_print_finished_event() { return EVT_PRINT_FINISHED; } -int Plater::get_print_finished_event() -{ - return EVT_PRINT_FINISHED; -} +int Plater::get_send_finished_event() { return EVT_SEND_FINISHED; } -int Plater::get_send_finished_event() -{ - return EVT_SEND_FINISHED; -} - -int Plater::get_publish_finished_event() -{ - return EVT_PUBLISH_FINISHED; -} +int Plater::get_publish_finished_event() { return EVT_PUBLISH_FINISHED; } void Plater::priv::set_current_canvas_as_dirty() { @@ -12103,10 +13734,10 @@ GLCanvas3D* Plater::priv::get_current_canvas3D(bool exclude_preview) return preview->get_canvas3d(); else if (current_panel == assemble_view) return assemble_view->get_canvas3d(); - else //BBS default set to view3D + else // BBS default set to view3D return view3D->get_canvas3d(); - //return (current_panel == view3D) ? view3D->get_canvas3d() : ((current_panel == preview) ? preview->get_canvas3d() : nullptr); + // return (current_panel == view3D) ? view3D->get_canvas3d() : ((current_panel == preview) ? preview->get_canvas3d() : nullptr); } void Plater::priv::unbind_canvas_event_handlers() @@ -12132,7 +13763,7 @@ void Plater::priv::reset_canvas_volumes() bool Plater::priv::check_ams_status_impl(bool is_slice_all) { - Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return true; @@ -12142,9 +13773,9 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) if (q->is_gcode_3mf() || q->only_gcode_mode() || q->get_partplate_list().get_curr_plate()->get_objects().empty()) { return true; } - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type()) { - bool is_same_as_printer = true; + bool is_same_as_printer = true; auto nozzle_volumes_values = preset_bundle->project_config.option("nozzle_volume_type")->values; assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2); if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) { @@ -12154,13 +13785,14 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) // After device sync, extruder_nozzle_stats matches printer → dialog suppressed. // Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp is_extruder_stat_synced() using namespace MultiNozzleUtils; - auto nozzle_diameter_values = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")->values; + auto nozzle_diameter_values = + preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")->values; // Build preset nozzle groups from extruder_nozzle_stats config std::vector> preset_nozzle_infos(nozzle_diameter_values.size()); for (size_t extruder_id = 0; extruder_id < nozzle_diameter_values.size(); ++extruder_id) { NozzleVolumeType preset_volume_type = NozzleVolumeType(nozzle_volumes_values[extruder_id]); - std::string preset_diameter = format_diameter_to_str(nozzle_diameter_values[extruder_id]); + std::string preset_diameter = format_diameter_to_str(nozzle_diameter_values[extruder_id]); if (preset_volume_type == nvtHybrid) { // Hybrid: expand into separate groups for each nozzle type from stats @@ -12183,9 +13815,9 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) for (const auto& preset_group : preset_groups) { if (preset_group.nozzle_count == 0) { // Never synced: if printer has nozzles of this type → needs sync - if (std::find_if(printer_groups.begin(), printer_groups.end(), - [&preset_group](const NozzleGroupInfo& elem) { return preset_group.is_same_type(elem); }) - != printer_groups.end()) { + if (std::find_if(printer_groups.begin(), printer_groups.end(), [&preset_group](const NozzleGroupInfo& elem) { + return preset_group.is_same_type(elem); + }) != printer_groups.end()) { is_same_as_printer = false; break; } @@ -12227,21 +13859,22 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) } if (!preset_bundle->extruder_ams_counts.empty() && !preset_bundle->extruder_ams_counts.front().empty()) { - is_same_as_printer &= preset_bundle->extruder_ams_counts[0][4] == left_4 - && preset_bundle->extruder_ams_counts[0][1] == left_1 - && preset_bundle->extruder_ams_counts[1][4] == right_4 - && preset_bundle->extruder_ams_counts[1][1] == right_1; + is_same_as_printer &= preset_bundle->extruder_ams_counts[0][4] == left_4 && + preset_bundle->extruder_ams_counts[0][1] == left_1 && + preset_bundle->extruder_ams_counts[1][4] == right_4 && + preset_bundle->extruder_ams_counts[1][1] == right_1; } if (!is_same_as_printer) { struct SyncInfoDialog : MessageDialog { - SyncInfoDialog(wxWindow *parent) + SyncInfoDialog(wxWindow* parent) : MessageDialog(parent, _L("The nozzle type and AMS quantity information has not been synced from the connected printer.\n" "After syncing, software can optimize printing time and filament usage when slicing.\n" "Would you like to sync now?"), - _L("Warning"), 0) + _L("Warning"), + 0) { add_button(wxID_YES, true, _L("Sync now")); add_button(wxID_NO, true, _L("Later")); @@ -12266,7 +13899,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) bool Plater::priv::get_machine_sync_status() { - Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return false; @@ -12274,7 +13907,7 @@ bool Plater::priv::get_machine_sync_status() if (!obj) return false; - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; return preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type(); } @@ -12289,10 +13922,10 @@ bool Plater::priv::init_collapse_toolbar() BackgroundTexture::Metadata background_data; background_data.filename = m_is_dark ? "toolbar_background_dark.png" : "toolbar_background.png"; - background_data.left = 16; - background_data.top = 16; - background_data.right = 16; - background_data.bottom = 16; + background_data.left = 16; + background_data.top = 16; + background_data.right = 16; + background_data.bottom = 16; if (!collapse_toolbar.init(background_data)) return false; @@ -12310,11 +13943,9 @@ bool Plater::priv::init_collapse_toolbar() item.name = "collapse_sidebar"; // set collapse svg name - item.icon_filename = "collapse.svg"; - item.sprite_id = 0; - item.left.action_callback = []() { - wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed()); - }; + item.icon_filename = "collapse.svg"; + item.sprite_id = 0; + item.left.action_callback = []() { wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed()); }; if (!collapse_toolbar.add_item(item)) return false; @@ -12325,10 +13956,7 @@ bool Plater::priv::init_collapse_toolbar() return true; } -void Plater::priv::update_preview_bottom_toolbar() -{ - ; -} +void Plater::priv::update_preview_bottom_toolbar() { ; } #if 0 void Plater::update_partplate() @@ -12337,32 +13965,25 @@ void Plater::update_partplate() } #endif -void Plater::priv::reset_gcode_toolpaths() -{ - preview->get_canvas3d()->reset_gcode_toolpaths(); -} +void Plater::priv::reset_gcode_toolpaths() { preview->get_canvas3d()->reset_gcode_toolpaths(); } bool Plater::priv::can_set_instance_to_object() const { const int obj_idx = get_selected_object_idx(); - return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->instances.size() > 1; + return 0 <= obj_idx && obj_idx < (int) model.objects.size() && model.objects[obj_idx]->instances.size() > 1; } -bool Plater::priv::can_split(bool to_objects) const -{ - return sidebar->obj_list()->is_splittable(to_objects); -} +bool Plater::priv::can_split(bool to_objects) const { return sidebar->obj_list()->is_splittable(to_objects); } bool Plater::priv::can_fillcolor() const { - //BBS TODO + // BBS TODO return true; } bool Plater::priv::has_assemble_view() const { - for (auto object: model.objects) - { + for (auto object : model.objects) { for (auto instance : object->instances) if (instance->is_assemble_initialized()) return true; @@ -12383,29 +14004,19 @@ bool Plater::priv::has_assemble_view() const bool Plater::priv::can_scale_to_print_volume() const { const BuildVolume_Type type = this->bed.build_volume().type(); - return !sidebar->obj_list()->has_selected_cut_object() - && !view3D->get_canvas3d()->get_selection().is_empty() - && (type == BuildVolume_Type::Rectangle || type == BuildVolume_Type::Circle); + return !sidebar->obj_list()->has_selected_cut_object() && !view3D->get_canvas3d()->get_selection().is_empty() && + (type == BuildVolume_Type::Rectangle || type == BuildVolume_Type::Circle); } #endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT bool Plater::priv::can_mirror() const -{ - return !sidebar->obj_list()->has_selected_cut_object() - && get_selection().is_from_single_instance(); -} +{ return !sidebar->obj_list()->has_selected_cut_object() && get_selection().is_from_single_instance(); } bool Plater::priv::can_replace_with_stl() const -{ - return !sidebar->obj_list()->has_selected_cut_object() - && get_selection().get_volume_idxs().size() == 1; -} +{ return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() == 1; } bool Plater::priv::can_replace_all_with_stl() const -{ - return !sidebar->obj_list()->has_selected_cut_object() - && get_selection().get_volume_idxs().size() != 1; -} +{ return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() != 1; } bool Plater::priv::can_reload_from_disk() const { @@ -12426,8 +14037,9 @@ bool Plater::priv::can_reload_from_disk() const int volume_idx; // operators needed by std::algorithms - bool operator < (const SelectedVolume& other) const { return (object_idx < other.object_idx) || ((object_idx == other.object_idx) && (volume_idx < other.volume_idx)); } - bool operator == (const SelectedVolume& other) const { return (object_idx == other.object_idx) && (volume_idx == other.volume_idx); } + bool operator<(const SelectedVolume& other) const + { return (object_idx < other.object_idx) || ((object_idx == other.object_idx) && (volume_idx < other.volume_idx)); } + bool operator==(const SelectedVolume& other) const { return (object_idx == other.object_idx) && (volume_idx == other.volume_idx); } }; std::vector selected_volumes; @@ -12437,22 +14049,24 @@ bool Plater::priv::can_reload_from_disk() const const std::set& selected_volumes_idxs = selection.get_volume_idxs(); for (unsigned int idx : selected_volumes_idxs) { const GLVolume* v = selection.get_volume(idx); - int v_idx = v->volume_idx(); + int v_idx = v->volume_idx(); if (v_idx >= 0) { int o_idx = v->object_idx(); - if (0 <= o_idx && o_idx < (int)model.objects.size()) - selected_volumes.push_back({ o_idx, v_idx }); + if (0 <= o_idx && o_idx < (int) model.objects.size()) + selected_volumes.push_back({o_idx, v_idx}); } } #endif // ENABLE_RELOAD_FROM_DISK_REWORK #if ENABLE_RELOAD_FROM_DISK_REWORK - std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair& v1, const std::pair& v2) { return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second); - }); - selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { - return (v1.first == v2.first) && (v1.second == v2.second); - }), selected_volumes.end()); + }); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), + [](const std::pair& v1, const std::pair& v2) { + return (v1.first == v2.first) && (v1.second == v2.second); + }), + selected_volumes.end()); // collects paths of files to load std::vector paths; @@ -12480,7 +14094,7 @@ bool Plater::priv::can_reload_from_disk() const return !paths.empty(); } -void Plater::priv::update_publish_dialog_status(wxString &msg, int percent) +void Plater::priv::update_publish_dialog_status(wxString& msg, int percent) { if (m_publish_dlg) m_publish_dlg->UpdateStatus(msg, percent); @@ -12488,42 +14102,45 @@ void Plater::priv::update_publish_dialog_status(wxString &msg, int percent) bool Plater::priv::show_publish_dlg(bool show) { - if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":recevied publish event\n"; } + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":recevied publish event\n"; + } - if (!m_publish_dlg) m_publish_dlg = new PublishDialog(q); + if (!m_publish_dlg) + m_publish_dlg = new PublishDialog(q); if (show) { m_publish_dlg->reset(); m_publish_dlg->start_slicing(); - //m_publish_dlg->Show(); + // m_publish_dlg->Show(); m_publish_dlg->ShowModal(); } else { m_publish_dlg->EndModal(wxID_OK); - //cancel the slicing + // cancel the slicing if (this->background_process.running()) this->background_process.stop(); } return true; } -//BBS: add bed exclude area -void Plater::priv::set_bed_shape(const Pointfs &shape, - const Pointfs &exclude_areas, - const Pointfs &wrapping_exclude_areas, - const double printable_height, +// BBS: add bed exclude area +void Plater::priv::set_bed_shape(const Pointfs& shape, + const Pointfs& exclude_areas, + const Pointfs& wrapping_exclude_areas, + const double printable_height, std::vector extruder_areas, - std::vector extruder_heights, - const std::string &custom_texture, - const std::string &custom_model, - bool force_as_custom) + std::vector extruder_heights, + const std::string& custom_texture, + const std::string& custom_model, + bool force_as_custom) { - //Orca: reduce resolution for large bed printer + // Orca: reduce resolution for large bed printer BoundingBoxf bed_size = get_extents(shape); if (bed_size.size().maxCoeff() <= LARGE_BED_THRESHOLD) SCALING_FACTOR = SCALING_FACTOR_INTERNAL; else SCALING_FACTOR = SCALING_FACTOR_INTERNAL_LARGE_PRINTER; - //BBS: add shape position + // BBS: add shape position Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); @@ -12532,25 +14149,28 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); double height_to_rod = config->opt_float("extruder_clearance_height_to_rod"); - Pointfs prev_exclude_areas = partplate_list.get_exclude_area(); + Pointfs prev_exclude_areas = partplate_list.get_exclude_area(); Pointfs prev_wrapping_exclude_areas = partplate_list.get_wrapping_exclude_area(); - new_shape |= (height_to_lid != prev_height_lid) || (height_to_rod != prev_height_rod) || (prev_exclude_areas != exclude_areas) - || (prev_wrapping_exclude_areas != wrapping_exclude_areas); + new_shape |= (height_to_lid != prev_height_lid) || (height_to_rod != prev_height_rod) || (prev_exclude_areas != exclude_areas) || + (prev_wrapping_exclude_areas != wrapping_exclude_areas); if (!new_shape && partplate_list.get_logo_texture_filename() != custom_texture) { partplate_list.update_logo_texture_filename(custom_texture); } if (new_shape) { - if (view3D) view3D->bed_shape_changed(); - if (preview) preview->bed_shape_changed(); + if (view3D) + view3D->bed_shape_changed(); + if (preview) + preview->bed_shape_changed(); - //BBS: update part plate's size - // BBS: to be checked + // BBS: update part plate's size + // BBS: to be checked Vec3d max = bed.printable_bounding_box().max; Vec3d min = bed.printable_bounding_box().min; - double z = config->opt_float("printable_height"); + double z = config->opt_float("printable_height"); partplate_list.reset_size(max.x() - min.x() - Bed3D::Axes::DefaultTipRadius, max.y() - min.y() - Bed3D::Axes::DefaultTipRadius, z); - partplate_list.set_shapes(shape, exclude_areas, wrapping_exclude_areas, extruder_areas, extruder_heights, custom_texture, height_to_lid, height_to_rod); + partplate_list.set_shapes(shape, exclude_areas, wrapping_exclude_areas, extruder_areas, extruder_heights, custom_texture, + height_to_lid, height_to_rod); Vec2d new_shape_position = partplate_list.get_current_shape_position(); if (shape_position != new_shape_position) @@ -12558,25 +14178,13 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, } } -bool Plater::priv::can_delete() const -{ - return !get_selection().is_empty() && !get_selection().is_wipe_tower(); -} +bool Plater::priv::can_delete() const { return !get_selection().is_empty() && !get_selection().is_wipe_tower(); } -bool Plater::priv::can_delete_all() const -{ - return !model.objects.empty(); -} +bool Plater::priv::can_delete_all() const { return !model.objects.empty(); } -bool Plater::priv::can_add_plate() const -{ - return q->get_partplate_list().get_plate_count() < PartPlateList::MAX_PLATES_COUNT; -} +bool Plater::priv::can_add_plate() const { return q->get_partplate_list().get_plate_count() < PartPlateList::MAX_PLATES_COUNT; } -bool Plater::priv::can_delete_plate() const -{ - return q->get_partplate_list().get_plate_count() > 1; -} +bool Plater::priv::can_delete_plate() const { return q->get_partplate_list().get_plate_count() > 1; } bool Plater::priv::can_fix_through_cgal() const { @@ -12585,8 +14193,8 @@ bool Plater::priv::can_fix_through_cgal() const #if FIX_THROUGH_CGAL_ALWAYS // Fixing always. - return ! obj_idxs.empty() || ! vol_idxs.empty(); -#else // FIX_THROUGH_CGAL_ALWAYS + return !obj_idxs.empty() || !vol_idxs.empty(); +#else // FIX_THROUGH_CGAL_ALWAYS // Fixing only if the model is not manifold. if (vol_idxs.empty()) { for (auto obj_idx : obj_idxs) @@ -12606,10 +14214,10 @@ bool Plater::priv::can_fix_through_cgal() const bool Plater::priv::can_simplify() const { // is object for simplification selected - if (get_selected_object_idx() < 0) return false; + if (get_selected_object_idx() < 0) + return false; // is already opened? - if (q->get_view3D_canvas3D()->get_gizmos_manager().get_current_type() == - GLGizmosManager::EType::Simplify) + if (q->get_view3D_canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::EType::Simplify) return false; return true; } @@ -12634,41 +14242,30 @@ bool Plater::priv::can_smooth_mesh() const bool Plater::priv::can_increase_instances() const { - if (!m_worker.is_idle() - || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) - return false; + if (!m_worker.is_idle() || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) + return false; int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) - && !sidebar->obj_list()->has_selected_cut_object() - && std::all_of(model.objects[obj_idx]->instances.begin(), model.objects[obj_idx]->instances.end(), [](auto& inst) {return inst->printable; }); + return (0 <= obj_idx) && (obj_idx < (int) model.objects.size()) && !sidebar->obj_list()->has_selected_cut_object() && + std::all_of(model.objects[obj_idx]->instances.begin(), model.objects[obj_idx]->instances.end(), + [](auto& inst) { return inst->printable; }); } bool Plater::priv::can_decrease_instances() const { - if (!m_worker.is_idle() - || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) - return false; + if (!m_worker.is_idle() || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) + return false; int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1) - && !sidebar->obj_list()->has_selected_cut_object(); + return (0 <= obj_idx) && (obj_idx < (int) model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1) && + !sidebar->obj_list()->has_selected_cut_object(); } -bool Plater::priv::can_split_to_objects() const -{ - return q->can_split(true); -} +bool Plater::priv::can_split_to_objects() const { return q->can_split(true); } -bool Plater::priv::can_split_to_volumes() const -{ - return (printer_technology != ptSLA) && q->can_split(false); -} +bool Plater::priv::can_split_to_volumes() const { return (printer_technology != ptSLA) && q->can_split(false); } -bool Plater::priv::can_arrange() const -{ - return !model.objects.empty() && m_worker.is_idle(); -} +bool Plater::priv::can_arrange() const { return !model.objects.empty() && m_worker.is_idle(); } bool Plater::priv::layers_height_allowed() const { @@ -12676,21 +14273,36 @@ bool Plater::priv::layers_height_allowed() const return false; int obj_idx = get_selected_object_idx(); - return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD && view3D->is_layers_editing_allowed(); + return 0 <= obj_idx && obj_idx < (int) model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD && + view3D->is_layers_editing_allowed(); } -bool Plater::priv::can_layers_editing() const -{ - return layers_height_allowed(); -} +bool Plater::priv::can_layers_editing() const { return layers_height_allowed(); } void Plater::priv::on_action_layersediting(SimpleEvent&) { + // Sub-layer splitting divides each layer by the mix ratio, so a variable layer height profile + // makes those sub-layer heights uneven and degrades the blend. ConfigManipulation warns for the + // opposite order, when the option is switched on while a variable profile already exists. + if (!view3D->is_layers_editing_enabled()) { + const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (print_config.opt_bool("enable_mixed_color_sublayer")) { + if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") { + MessageDialog + dlg(q, _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), + _L("Warning"), wxICON_WARNING | wxOK); + dlg.show_dsa_button(); + dlg.ShowModal(); + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1"); + } + } + } view3D->enable_layers_editing(!view3D->is_layers_editing_enabled()); notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled()); } -void Plater::priv::on_create_filament(SimpleEvent &) +void Plater::priv::on_create_filament(SimpleEvent&) { CreateFilamentPresetDialog dlg(wxGetApp().mainframe); int res = dlg.ShowModal(); @@ -12699,51 +14311,48 @@ void Plater::priv::on_create_filament(SimpleEvent &) update_ui_from_settings(); sidebar->update_all_preset_comboboxes(); CreatePresetSuccessfulDialog success_dlg(wxGetApp().mainframe, SuccessType::FILAMENT); - int res = success_dlg.ShowModal(); + int res = success_dlg.ShowModal(); } } -void Plater::priv::on_modify_filament(SimpleEvent &evt) +void Plater::priv::on_modify_filament(SimpleEvent& evt) { - Filamentinformation *filament_info = static_cast(evt.GetEventObject()); - int res; + Filamentinformation* filament_info = static_cast(evt.GetEventObject()); + int res; std::shared_ptr need_edit_preset; { EditFilamentPresetDialog dlg(wxGetApp().mainframe, filament_info); - res = dlg.ShowModal(); + res = dlg.ShowModal(); need_edit_preset = dlg.get_need_edit_preset(); } wxGetApp().mainframe->update_side_preset_ui(); update_ui_from_settings(); sidebar->update_all_preset_comboboxes(); if (wxID_EDIT == res) { - Tab *tab = wxGetApp().get_tab(Preset::Type::TYPE_FILAMENT); - //tab->restore_last_select_item(); - if (tab == nullptr) { return; } + Tab* tab = wxGetApp().get_tab(Preset::Type::TYPE_FILAMENT); + // tab->restore_last_select_item(); + if (tab == nullptr) { + return; + } // Popup needs to be called before "restore_last_select_item", otherwise the page may not be updated wxGetApp().params_dialog()->Popup(); tab->restore_last_select_item(); - // Opening Studio and directly accessing the Filament settings interface through the edit preset button will not take effect and requires manual settings. + // Opening Studio and directly accessing the Filament settings interface through the edit preset button will not take effect and + // requires manual settings. tab->set_just_edit(true); tab->select_preset(need_edit_preset->name); - // when some preset have modified, if the printer is not need_edit_preset_name compatible printer, the preset will jump to other preset, need select again - if (!need_edit_preset->is_compatible) tab->select_preset(need_edit_preset->name); + // when some preset have modified, if the printer is not need_edit_preset_name compatible printer, the preset will jump to other + // preset, need select again + if (!need_edit_preset->is_compatible) + tab->select_preset(need_edit_preset->name); } - } -void Plater::priv::on_add_filament(SimpleEvent &evt) { - sidebar->add_filament(); -} +void Plater::priv::on_add_filament(SimpleEvent& evt) { sidebar->add_filament(); } -void Plater::priv::on_delete_filament(SimpleEvent &evt) { - sidebar->delete_filament(); -} +void Plater::priv::on_delete_filament(SimpleEvent& evt) { sidebar->delete_filament(); } -void Plater::priv::on_add_custom_filament(ColorEvent &evt) -{ - sidebar->add_custom_filament(evt.data); -} +void Plater::priv::on_add_custom_filament(ColorEvent& evt) { sidebar->add_custom_filament(evt.data); } void Plater::priv::enter_gizmos_stack() { @@ -12762,7 +14371,7 @@ bool Plater::priv::leave_gizmos_stack() bool changed = false; assert(m_undo_redo_stack_active == &m_undo_redo_stack_gizmos); if (m_undo_redo_stack_active == &m_undo_redo_stack_gizmos) { - assert(! m_undo_redo_stack_active->empty()); + assert(!m_undo_redo_stack_active->empty()); changed = m_undo_redo_stack_gizmos.has_undo_snapshot(); m_undo_redo_stack_active->clear(); m_undo_redo_stack_active = &m_undo_redo_stack_main; @@ -12772,7 +14381,7 @@ bool Plater::priv::leave_gizmos_stack() int Plater::priv::get_active_snapshot_index() { - const size_t active_snapshot_time = this->undo_redo_stack().active_snapshot_time(); + const size_t active_snapshot_time = this->undo_redo_stack().active_snapshot_time(); const std::vector& ss_stack = this->undo_redo_stack().snapshots(); const auto it = std::lower_bound(ss_stack.begin(), ss_stack.end(), UndoRedo::Snapshot(active_snapshot_time)); return it - ss_stack.begin(); @@ -12794,12 +14403,10 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed if (this->sidebar->obj_list()->is_selected(itSettings)) { snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR; snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } - else if (this->sidebar->obj_list()->is_selected(itLayer)) { + } else if (this->sidebar->obj_list()->is_selected(itLayer)) { snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR; snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } - else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) + } else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR; // If SLA gizmo is active, ask it if it wants to trigger support generation @@ -12807,12 +14414,12 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed if (view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; - //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. - // BBS: add partplate logic + // FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print + // config. BBS: add partplate logic if (this->printer_technology == ptFFF) { - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; const ConfigOptionFloats* tower_x_opt = proj_cfg.option("wipe_tower_x"); const ConfigOptionFloats* tower_y_opt = proj_cfg.option("wipe_tower_y"); assert(tower_x_opt->values.size() == tower_y_opt->values.size()); @@ -12822,18 +14429,24 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = config.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } - const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager(); + const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_gizmos_manager() : + view3D->get_canvas3d()->get_gizmos_manager(); if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator) this->undo_redo_stack().clear(); - this->undo_redo_stack().take_snapshot(snapshot_name, model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection() : view3D->get_canvas3d()->get_selection(), gizmos, partplate_list, snapshot_data); + this->undo_redo_stack().take_snapshot(snapshot_name, model, + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_selection() : + view3D->get_canvas3d()->get_selection(), + gizmos, partplate_list, snapshot_data); if (snapshot_type == UndoRedo::SnapshotType::LeavingGizmoWithAction) { - // Filter all but the last UndoRedo::SnapshotType::GizmoAction in a row between the last UndoRedo::SnapshotType::EnteringGizmo and UndoRedo::SnapshotType::LeavingGizmoWithAction. - // The remaining snapshot will be renamed to a more generic name, - // depending on what gizmo is being left. + // Filter all but the last UndoRedo::SnapshotType::GizmoAction in a row between the last UndoRedo::SnapshotType::EnteringGizmo and + // UndoRedo::SnapshotType::LeavingGizmoWithAction. The remaining snapshot will be renamed to a more generic name, depending on what + // gizmo is being left. if (gizmos.get_current() != nullptr) { std::string new_name = gizmos.get_current()->get_action_snapshot_name(); this->undo_redo_stack().reduce_noisy_snapshots(new_name); @@ -12842,23 +14455,29 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed // Reset the "dirty project" flag. m_undo_redo_stack_main.mark_current_as_saved(); } - //BBS: add PartPlateList as the paremeter for take_snapshot + // BBS: add PartPlateList as the paremeter for take_snapshot this->undo_redo_stack().release_least_recently_used(); dirty_state.update_from_undo_redo_stack(m_undo_redo_stack_main.project_modified()); // Save the last active preset name of a particular printer technology. - ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); - BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); + ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : + m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name + << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) + << log_memory_info(); } void Plater::priv::undo() { - const std::vector &snapshots = this->undo_redo_stack().snapshots(); - auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); + const std::vector& snapshots = this->undo_redo_stack().snapshots(); + auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), + UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); // BBS: undo-redo until modify record - while (--it_current != snapshots.begin() && !snapshot_modifies_project(*it_current)); - if (it_current == snapshots.begin()) return; + while (--it_current != snapshots.begin() && !snapshot_modifies_project(*it_current)) + ; + if (it_current == snapshots.begin()) + return; if (get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView) { if (it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::GizmoAction && it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::EnteringGizmo && @@ -12871,19 +14490,22 @@ void Plater::priv::undo() void Plater::priv::redo() { - const std::vector &snapshots = this->undo_redo_stack().snapshots(); - auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); + const std::vector& snapshots = this->undo_redo_stack().snapshots(); + auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), + UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); // BBS: undo-redo until modify record - while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)); + while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)) + ; if (it_current != snapshots.end()) { - while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)); + while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)) + ; this->undo_redo_to(--it_current); } } void Plater::priv::undo_redo_to(size_t time_to_load) { - const std::vector &snapshots = this->undo_redo_stack().snapshots(); + const std::vector& snapshots = this->undo_redo_stack().snapshots(); auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(time_to_load)); assert(it_current != snapshots.end()); this->undo_redo_to(it_current); @@ -12898,8 +14520,7 @@ bool Plater::priv::up_to_date(bool saved, bool backup) if (!backup) undo_redo_stack_main().mark_current_as_saved(); return true; - } - else { + } else { return !undo_redo_stack_main().has_real_change_from(last_time); } } @@ -12909,20 +14530,21 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator // Make sure that no updating function calls take_snapshot until we are done. SuppressSnapshots snapshot_supressor(q); - bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active(); - PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology; - bool printer_technology_changed = this->printer_technology != new_printer_technology; + bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active(); + PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology; + bool printer_technology_changed = this->printer_technology != new_printer_technology; if (printer_technology_changed) { - //BBS do not support SLA + // BBS do not support SLA } // Save the last active preset name of a particular printer technology. - ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); - //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. - // BBS: add partplate logic + ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : + m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); + // FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print + // config. BBS: add partplate logic if (this->printer_technology == ptFFF) { - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; const ConfigOptionFloats* tower_x_opt = proj_cfg.option("wipe_tower_x"); const ConfigOptionFloats* tower_y_opt = proj_cfg.option("wipe_tower_y"); assert(tower_x_opt->values.size() == tower_y_opt->values.size()); @@ -12932,7 +14554,7 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = config.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; @@ -12945,17 +14567,15 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator if (this->sidebar->obj_list()->is_selected(itSettings)) { top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR; top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } - else if (this->sidebar->obj_list()->is_selected(itLayer)) { + } else if (this->sidebar->obj_list()->is_selected(itLayer)) { top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR; top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } - else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) + } else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR; - bool new_variable_layer_editing_active = (new_flags & UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE) != 0; - bool new_selected_settings_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR) != 0; - bool new_selected_layer_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR) != 0; - bool new_selected_layerroot_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR) != 0; + bool new_variable_layer_editing_active = (new_flags & UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE) != 0; + bool new_selected_settings_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR) != 0; + bool new_selected_layer_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR) != 0; + bool new_selected_layerroot_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR) != 0; if (this->view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) top_snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; @@ -12968,32 +14588,45 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator const UndoRedo::Snapshot snapshot_copy = *it_snapshot; // Do the jump in time. if (it_snapshot->timestamp < this->undo_redo_stack().active_snapshot_time() ? - this->undo_redo_stack().undo(model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection() : this->view3D->get_canvas3d()->get_selection(), get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : this->view3D->get_canvas3d()->get_gizmos_manager(), this->partplate_list, top_snapshot_data, it_snapshot->timestamp) : - this->undo_redo_stack().redo(model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : this->view3D->get_canvas3d()->get_gizmos_manager(), this->partplate_list, it_snapshot->timestamp)) { + this->undo_redo_stack().undo(model, + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_selection() : + this->view3D->get_canvas3d()->get_selection(), + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_gizmos_manager() : + this->view3D->get_canvas3d()->get_gizmos_manager(), + this->partplate_list, top_snapshot_data, it_snapshot->timestamp) : + this->undo_redo_stack().redo(model, + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_gizmos_manager() : + this->view3D->get_canvas3d()->get_gizmos_manager(), + this->partplate_list, it_snapshot->timestamp)) { if (printer_technology_changed) { // Switch to the other printer technology. Switch to the last printer active for that particular technology. - AppConfig *app_config = wxGetApp().app_config; - app_config->set("presets", PRESET_PRINTER_NAME, (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); - //FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. - // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. + AppConfig* app_config = wxGetApp().app_config; + app_config->set("presets", PRESET_PRINTER_NAME, + (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); + // FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. + // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); - // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(), - // but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first. + // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> + // Object_list::update_and_show_object_settings_item(), but the Object list still keeps pointer to the old Model. Avoid a crash + // by removing selection first. this->sidebar->obj_list()->unselect_objects(); // Load the currently selected preset into the GUI, update the preset selection box. // This also switches the printer technology based on the printer technology of the active printer profile. wxGetApp().load_current_presets(); } - //FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. - // BBS: add partplate logic + // FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print + // config. BBS: add partplate logic if (this->printer_technology == ptFFF) { - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; - ConfigOptionFloats* tower_x_opt = const_cast(proj_cfg.option("wipe_tower_x")); - ConfigOptionFloats* tower_y_opt = const_cast(proj_cfg.option("wipe_tower_y")); + ConfigOptionFloats* tower_x_opt = const_cast(proj_cfg.option("wipe_tower_x")); + ConfigOptionFloats* tower_y_opt = const_cast(proj_cfg.option("wipe_tower_y")); // BBS: don't support wipe tower rotation - //double current_rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + // double current_rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); bool need_update = false; if (tower_x_opt->values.size() != model.wipe_tower.positions.size()) { tower_x_opt->clear(); @@ -13047,18 +14680,28 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */) { - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection().clear() : this->view3D->get_canvas3d()->get_selection().clear(); + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection().clear() : + this->view3D->get_canvas3d()->get_selection().clear(); // Update volumes from the deserializd model, always stop / update the background processing (for both the SLA and FFF technologies). - this->update((unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE | (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); - // Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first snapshot. - //if (temp_snapshot_was_taken) - // Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while some - // triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the Undo / Redo stack size. - this->undo_redo_stack().release_least_recently_used(); - //YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time) - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances) : - this->view3D->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances); + this->update((unsigned int) UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE | + (unsigned int) UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + // Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first + // snapshot. + // if (temp_snapshot_was_taken) + // Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while + // some triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the + // Undo / Redo stack size. + this->undo_redo_stack().release_least_recently_used(); + // YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time) + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d() + ->get_selection() + .set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), + this->undo_redo_stack().selection_deserialized().volumes_and_instances) : + this->view3D->get_canvas3d() + ->get_selection() + .set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), + this->undo_redo_stack().selection_deserialized().volumes_and_instances); get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot) : this->view3D->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot); @@ -13067,20 +14710,23 @@ void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bo if (wxGetApp().get_mode() == comSimple && model_has_advanced_features(this->model)) { // If the user jumped to a snapshot that require user interface with advanced features, switch to the advanced mode without asking. - // There is a little risk of surprising the user, as he already must have had the advanced or advanced mode active for such a snapshot to be taken. + // There is a little risk of surprising the user, as he already must have had the advanced or advanced mode active for such a + // snapshot to be taken. Slic3r::GUI::wxGetApp().save_mode(comAdvanced); view3D->set_as_dirty(); } - // this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the back end, it would not open immediately, - // but it would be saved to be show later. Let's do it now. We do not want to display the message box earlier, because on Windows & OSX the message box takes over the message - // queue pump, which in turn executes the rendering function before a full update after the Undo / Redo jump. + // this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the + // back end, it would not open immediately, but it would be saved to be show later. Let's do it now. We do not want to display the + // message box earlier, because on Windows & OSX the message box takes over the message queue pump, which in turn executes the rendering + // function before a full update after the Undo / Redo jump. this->show_delayed_error_message(); - //FIXME what about the state of the manipulators? - //FIXME what about the focus? Cursor in the side panel? + // FIXME what about the state of the manipulators? + // FIXME what about the focus? Cursor in the side panel? - BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " + << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); } void Plater::priv::bring_instance_forward() const @@ -13094,7 +14740,7 @@ void Plater::priv::bring_instance_forward() const return; } BOOST_LOG_TRIVIAL(debug) << "Orca Slicer window going forward"; - //this code maximize app window on Fedora + // this code maximize app window on Fedora { main_frame->Iconize(false); if (main_frame->IsMaximized()) @@ -13102,25 +14748,25 @@ void Plater::priv::bring_instance_forward() const else main_frame->Maximize(false); } - //this code maximize window on Ubuntu + // this code maximize window on Ubuntu { main_frame->Restore(); - wxGetApp().GetTopWindow()->SetFocus(); // focus on my window + wxGetApp().GetTopWindow()->SetFocus(); // focus on my window wxGetApp().GetTopWindow()->Show(true); // show the window - wxGetApp().GetTopWindow()->Raise(); // bring window to front + wxGetApp().GetTopWindow()->Raise(); // bring window to front } } -//BBS: popup object table +// BBS: popup object table bool Plater::priv::PopupObjectTable(int object_id, int volume_id, const wxPoint& position) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, create ObjectTableDialog"); int max_width{1920}, max_height{1080}; - max_width = q->GetMaxWidth(); + max_width = q->GetMaxWidth(); max_height = q->GetMaxHeight(); ObjectTableDialog table_dialog(q, q, &model, wxSize(max_width, max_height)); - //m_popup_table = new ObjectTableDialog(q, q, &model); + // m_popup_table = new ObjectTableDialog(q, q, &model); wxRect rect = sidebar->GetRect(); wxPoint pos = sidebar->ClientToScreen(wxPoint(rect.x, rect.y)); @@ -13134,19 +14780,17 @@ bool Plater::priv::PopupObjectTable(int object_id, int volume_id, const wxPoint& void Sidebar::set_btn_label(const ActionButtonType btn_type, const wxString& label) const { - switch (btn_type) - { - case ActionButtonType::abReslice: p->btn_reslice->SetLabelText(label); break; - case ActionButtonType::abExport: p->btn_export_gcode->SetLabelText(label); break; - case ActionButtonType::abSendGCode: /*p->btn_send_gcode->SetLabelText(label);*/ break; + switch (btn_type) { + case ActionButtonType::abReslice: p->btn_reslice->SetLabelText(label); break; + case ActionButtonType::abExport: p->btn_export_gcode->SetLabelText(label); break; + case ActionButtonType::abSendGCode: /*p->btn_send_gcode->SetLabelText(label);*/ break; } } // Plater / Public -Plater::Plater(wxWindow *parent, MainFrame *main_frame) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size()) - , p(new priv(this, main_frame)) +Plater::Plater(wxWindow* parent, MainFrame* main_frame) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size()), p(new priv(this, main_frame)) { // Initialization performed in the private c-tor enable_wireframe(true); @@ -13164,20 +14808,368 @@ bool Plater::is_project_dirty() const { return p->is_project_dirty(); } bool Plater::is_presets_dirty() const { return p->is_presets_dirty(); } void Plater::set_plater_dirty(bool is_dirty) { p->set_plater_dirty(is_dirty); } void Plater::update_project_dirty_from_presets() { p->update_project_dirty_from_presets(); } -int Plater::save_project_if_dirty(const wxString& reason) { return p->save_project_if_dirty(reason); } +int Plater::save_project_if_dirty(const wxString& reason) { return p->save_project_if_dirty(reason); } void Plater::reset_project_dirty_after_save() { p->reset_project_dirty_after_save(); } void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_initial_presets(); } #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW void Plater::render_project_state_debug_window() const { p->render_project_state_debug_window(); } #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW -Sidebar& Plater::sidebar() { return *p->sidebar; } -const Model& Plater::model() const { return p->model; } -Model& Plater::model() { return p->model; } -const Print& Plater::fff_print() const { return p->fff_print; } -Print& Plater::fff_print() { return p->fff_print; } -const SLAPrint& Plater::sla_print() const { return p->sla_print; } -SLAPrint& Plater::sla_print() { return p->sla_print; } +std::vector Plater::mixed_filament_config_indices() const +{ + std::vector indices; + auto& config = wxGetApp().preset_bundle->project_config; + auto* opt = config.option("filament_is_mixed"); + if (!opt) + return indices; + for (size_t i = 0; i < opt->values.size(); ++i) + if (opt->values[i]) + indices.push_back(i); + return indices; +} + +std::vector Plater::physical_filament_config_indices() const +{ + std::vector indices; + auto& config = wxGetApp().preset_bundle->project_config; + auto* opt = config.option("filament_is_mixed"); + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + for (size_t i = 0; i < total; ++i) { + if (!opt || i >= opt->values.size() || !opt->values[i]) + indices.push_back(i); + } + return indices; +} + +bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, + TextureImportResult& result, + std::function cancel_callback, + std::function progress_callback) +{ + if (!loaded_model.texture_mesh || !has_importable_texture(*loaded_model.texture_mesh)) + return false; + + // Defense in depth: if all geometry got dropped earlier (e.g. by a future + // regression of the zero-volume cleanup) but the textured mesh is still + // alive, there is nothing for the dialog to paint onto. Skip the dialog + // gracefully so load_files() can fall through to its "no geometry" + // message instead of making the user round-trip a meaningless matcher. + if (loaded_model.objects.empty()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: skipping dialog because the loaded model has no geometry objects"; + loaded_model.texture_mesh.reset(); + result.skipped = true; + return true; + } + + const wxString fallback_warning = _L("Texture import failed. The model appears to contain texture data, but the texture import process " + "could not be completed. The model will be imported as geometry only."); + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: opening texture import dialog"; + + std::vector filament_entries; + { + auto& preset_bundle = *wxGetApp().preset_bundle; + auto& project_config = preset_bundle.project_config; + auto* colours_opt = project_config.option("filament_colour"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* type_opt = project_config.option("filament_type"); + auto* components_opt = project_config.option("filament_mixed_components"); + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + const size_t total = preset_bundle.filament_presets.size(); + filament_entries.reserve(total); + for (size_t i = 0; i < total; ++i) { + TextureFilamentEntry entry; + entry.kind = (is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]) ? + TextureFilamentKind::ExistingMixed : + TextureFilamentKind::ExistingPhysical; + entry.dialog_index = (int) filament_entries.size(); + entry.project_config_index = i; + entry.color_hex = (colours_opt && i < colours_opt->values.size()) ? colours_opt->values[i] : "#808080"; + entry.type = (type_opt && i < type_opt->values.size()) ? type_opt->values[i] : ""; + + std::string name; + if (i < preset_bundle.filament_presets.size()) { + auto* preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[i]); + if (preset) + name = preset->label(false); + } + if (name.empty()) + name = "Filament " + std::to_string(i + 1); + entry.name = name; + + if (entry.kind == TextureFilamentKind::ExistingMixed) { + if (components_opt && i < components_opt->values.size()) + entry.mixed_components = Slic3r::parse_mixed_components(components_opt->values[i]); + std::vector ratios = Slic3r::parse_mixed_ratios(ratios_opt && i < ratios_opt->values.size() ? + ratios_opt->values[i] : + "", + entry.mixed_components.size()); + entry.mixed_ratios.reserve(ratios.size()); + for (double ratio : ratios) + entry.mixed_ratios.push_back((int) std::lround(ratio * 100.0)); + } + filament_entries.push_back(std::move(entry)); + } + } + + TextureImportDialog dlg(q, *loaded_model.texture_mesh, filament_entries, std::move(cancel_callback), std::move(progress_callback)); + if (dlg.ShowModal() != wxID_OK) { + if (dlg.was_skipped()) { + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: user skipped texture matching"; + result.skipped = true; + loaded_model.texture_mesh.reset(); + return true; + } + if (dlg.fallback_to_geometry_only()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: texture import failed, falling back to geometry-only import"; + result.fallback_to_geometry_only = true; + result.fallback_warning = fallback_warning; + loaded_model.texture_mesh.reset(); + return true; + } + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: user cancelled"; + loaded_model.texture_mesh.reset(); + return false; + } + + auto painted = dlg.get_painted_mesh(); + auto final_matches = dlg.get_matches(); + + if (painted.face_colors.empty() || final_matches.empty()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: no painting result"; + result.fallback_to_geometry_only = true; + result.fallback_warning = fallback_warning; + loaded_model.texture_mesh.reset(); + return true; + } + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: got " << painted.cluster_colors.size() + << " clusters, skipped=" << dlg.was_skipped(); + + result.painted = std::move(painted); + result.matches = std::move(final_matches); + result.new_filament_colors = dlg.get_new_filament_colors(); + result.new_filament_preset_names = dlg.get_new_filament_preset_names(); + result.new_mixed_filaments = dlg.get_new_mixed_filaments(); + result.filament_entries = dlg.get_filament_entries(); + result.existing_filament_count = dlg.get_existing_filament_count(); + result.skipped = dlg.was_skipped(); + return true; +} + +void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model, + const std::vector& obj_idxs, + const TextureImportResult& result, + LoadProgressCallback progress_callback, + bool update_scene) +{ + auto update_apply_progress = [&progress_callback](int percent, const wxString& message) { + return !progress_callback || progress_callback(std::clamp(percent, 0, 100), message); + }; + + const auto& painted = result.painted; + const auto& final_matches = result.matches; + + if (painted.face_colors.empty() || final_matches.empty()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: no painting result"; + loaded_model.texture_mesh.reset(); + return; + } + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: got " << painted.cluster_colors.size() + << " clusters, skipped=" << result.skipped; + if (!update_apply_progress(0, _L("Applying texture colors..."))) + return; + + auto collect_physical_color_strs = []() { + std::vector colors; + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* colours_opt = project_config.option("filament_colour"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + const size_t total = wxGetApp().preset_bundle->filament_presets.size(); + for (size_t i = 0; i < total; ++i) { + const bool is_mixed = is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]; + if (!is_mixed) + colors.push_back(colours_opt && i < colours_opt->values.size() ? colours_opt->values[i] : "#808080"); + } + return colors; + }; + + const auto& entries = result.filament_entries; + std::vector filament_index_remap(entries.size(), -1); + size_t existing_physical_count = 0; + size_t new_physical_count = 0; + for (const auto& entry : entries) { + if (entry.kind == TextureFilamentKind::ExistingPhysical) + ++existing_physical_count; + else if (entry.kind == TextureFilamentKind::NewPhysical) + ++new_physical_count; + } + + for (const auto& entry : entries) { + if (entry.dialog_index < 0 || entry.dialog_index >= (int) filament_index_remap.size()) + continue; + if (entry.kind == TextureFilamentKind::ExistingPhysical) { + filament_index_remap[entry.dialog_index] = (int) entry.project_config_index; + } else if (entry.kind == TextureFilamentKind::ExistingMixed) { + filament_index_remap[entry.dialog_index] = (int) (entry.project_config_index + new_physical_count); + } + } + + size_t new_physical_order = 0; + for (const auto& entry : entries) { + if (entry.kind != TextureFilamentKind::NewPhysical) + continue; + wxColour new_col(entry.color_hex); + const size_t final_idx = existing_physical_count + new_physical_order; + sidebar->add_custom_filament(new_col, entry.preset_name); + if (entry.dialog_index >= 0 && entry.dialog_index < (int) filament_index_remap.size()) + filament_index_remap[entry.dialog_index] = (int) final_idx; + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending physical filament dialog=" << entry.dialog_index + << " final=" << final_idx << " color=" << entry.color_hex << " preset=" << entry.preset_name; + ++new_physical_order; + } + + std::vector physical_colors_for_mixing = collect_physical_color_strs(); + for (const auto& mixed : result.new_mixed_filaments) { + MixedFilamentResult mixed_result; + mixed_result.ratios = mixed.ratios; + mixed_result.components.reserve(mixed.component_dialog_indices.size()); + bool valid_components = true; + for (int component_dialog_idx : mixed.component_dialog_indices) { + if (component_dialog_idx < 0 || component_dialog_idx >= (int) filament_index_remap.size() || + filament_index_remap[component_dialog_idx] < 0) { + valid_components = false; + break; + } + mixed_result.components.push_back((unsigned int) (filament_index_remap[component_dialog_idx] + 1)); + } + if (!valid_components || mixed_result.components.size() < 2 || mixed_result.components.size() != mixed_result.ratios.size()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid pending mixed filament dialog=" << mixed.dialog_index; + continue; + } + + const int final_idx = (int) wxGetApp().preset_bundle->filament_presets.size(); + if (create_mixed_filament_from_result(sidebar, mixed_result, physical_colors_for_mixing)) { + if (mixed.dialog_index >= 0 && mixed.dialog_index < (int) filament_index_remap.size()) + filament_index_remap[mixed.dialog_index] = final_idx; + physical_colors_for_mixing = collect_physical_color_strs(); + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending mixed filament dialog=" << mixed.dialog_index + << " final=" << final_idx; + } + } + + std::vector remapped_matches = final_matches; + for (auto& m : remapped_matches) { + if (m.filament_index < 0) + continue; + if (m.filament_index < (int) filament_index_remap.size() && filament_index_remap[m.filament_index] >= 0) { + m.filament_index = filament_index_remap[m.filament_index]; + } else { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid filament index " << m.filament_index + << " in texture mapping"; + m.filament_index = -1; + } + } + + int min_used_filament_1based = -1; + { + std::map, int> color_to_filament; + for (const auto& m : remapped_matches) { + if (m.cluster_index >= 0 && m.cluster_index < (int) painted.cluster_colors.size() && m.filament_index >= 0) + color_to_filament[painted.cluster_colors[m.cluster_index]] = m.filament_index + 1; + } + for (const auto& face_color : painted.face_colors) { + auto it = color_to_filament.find(face_color); + if (it == color_to_filament.end()) + continue; + if (min_used_filament_1based < 0 || it->second < min_used_filament_1based) + min_used_filament_1based = it->second; + } + } + if (min_used_filament_1based < 0) + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: cannot determine base filament from painted faces"; + + if (!update_apply_progress(25, _L("Applying texture colors..."))) + return; + + for (size_t obj_order = 0; obj_order < obj_idxs.size(); ++obj_order) { + size_t idx = obj_idxs[obj_order]; + if (idx >= loaded_model.objects.size()) + continue; + ModelObject* obj = loaded_model.objects[idx]; + if (!obj) + continue; + + // painted is derived from the whole textured mesh and is meaningful + // only against a single MODEL_PART volume. Applying it to every + // volume of a multi-part / modifier object would overwrite each + // volume with the same painted geometry. Restrict to the first + // model_part and warn when the object holds more than one. + ModelVolume* target = nullptr; + int part_count = 0; + for (ModelVolume* vol : obj->volumes) { + if (vol && vol->is_model_part()) { + ++part_count; + if (!target) + target = vol; + } + } + if (!target) + continue; + if (part_count > 1) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: object has " << part_count + << " model parts; painting only applied to the first part."; + } + if (Slic3r::apply_painted_mesh_to_volume(painted, remapped_matches, *target) && min_used_filament_1based > 0) { + target->config.set("extruder", min_used_filament_1based); + obj->config.set("extruder", min_used_filament_1based); + if (update_scene) { + if (auto* obj_list = wxGetApp().obj_list()) { + obj_list->update_objects_list_filament_column( + std::max(wxGetApp().filaments_cnt(), (size_t) min_used_filament_1based)); + obj_list->update_info_items(idx); + } + } + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: set base filament to " << min_used_filament_1based + << " for object index " << idx << ", object extruder=" << obj->config.extruder() + << ", volume extruder=" << target->config.extruder(); + } + // bbox invalidation is performed inside apply_painted_mesh_to_volume. + obj->ensure_on_bed(); + const int object_percent = 25 + (int) (60 * (obj_order + 1) / std::max(obj_idxs.size(), 1)); + if (!update_apply_progress(object_percent, _L("Applying texture colors..."))) + return; + } + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: painting applied to model volumes"; + loaded_model.texture_mesh.reset(); + if (update_scene) { + if (!update_apply_progress(90, _L("Updating 3D view..."))) + return; + update(); + } + update_apply_progress(100, _L("Texture colors applied.")); +} + +void Plater::priv::handle_textured_mesh_import(Slic3r::Model& loaded_model, + const std::vector& obj_idxs, + std::function cancel_callback) +{ + TextureImportResult result; + if (!run_textured_mesh_import_dialog(loaded_model, result, std::move(cancel_callback))) + return; + if (!result.painted.face_colors.empty()) + apply_textured_mesh_import_result(loaded_model, obj_idxs, result); +} + +Sidebar& Plater::sidebar() { return *p->sidebar; } +const Model& Plater::model() const { return p->model; } +Model& Plater::model() { return p->model; } +const Print& Plater::fff_print() const { return p->fff_print; } +Print& Plater::fff_print() { return p->fff_print; } +const SLAPrint& Plater::sla_print() const { return p->sla_print; } +SLAPrint& Plater::sla_print() { return p->sla_print; } int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_name) { @@ -13186,18 +15178,20 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ bool transfer_preset_changes = false; // BBS: save confirm - auto check = [this,&transfer_preset_changes](bool yes_or_no) { + auto check = [this, &transfer_preset_changes](bool yes_or_no) { m_new_project_and_check_state = true; wxString header = _L("Some presets are modified.") + "\n" + - (yes_or_no ? _L("You can keep the modified presets for the new project or discard them") : - _L("You can keep the modified presets for the new project, discard, or save changes as new presets.")); + (yes_or_no ? + _L("You can keep the modified presets for the new project or discard them") : + _L("You can keep the modified presets for the new project, discard, or save changes as new presets.")); int act_buttons = ActionButtons::KEEP | ActionButtons::REMEMBER_CHOISE; if (!yes_or_no) act_buttons |= ActionButtons::SAVE; if (m_exported_file) { //.gcode.3mf ignore presets modify m_exported_file = false; } - bool result = wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons, &transfer_preset_changes); + bool result = wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons, + &transfer_preset_changes); m_new_project_and_check_state = false; return result; }; @@ -13205,26 +15199,26 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ if (!skip_confirm && (result = close_with_confirm(check)) == wxID_CANCEL) return wxID_CANCEL; - m_only_gcode = false; - m_exported_file = false; + m_only_gcode = false; + m_exported_file = false; m_loading_project = false; get_notification_manager()->clear_all(); if (!silent) wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - //get_partplate_list().reinit(); - //get_partplate_list().update_slice_context_to_current_plate(p->background_process); - //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + // get_partplate_list().reinit(); + // get_partplate_list().update_slice_context_to_current_plate(p->background_process); + // p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); reset(transfer_preset_changes); reset_project_dirty_after_save(); reset_project_dirty_initial_presets(); wxGetApp().update_saved_preset_from_current_preset(); update_project_dirty_from_presets(); - //reset project + // reset project p->project.reset(); - //set project name + // set project name if (project_name.empty()) p->set_project_name(_L("Untitled")); else @@ -13235,7 +15229,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ Model m; model().load_from(m); // new id avoid same path name - //select first plate + // select first plate get_partplate_list().select_plate(0); SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); @@ -13257,8 +15251,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ LoadType determine_load_type(std::string filename, std::string override_setting = ""); // BBS: FIXME, missing resotre logic -void Plater::load_project(wxString const& filename2, - wxString const& originfile) +void Plater::load_project(wxString const& filename2, wxString const& originfile) { model().calib_pa_pattern.reset(nullptr); model().plates_custom_gcodes.clear(); @@ -13266,9 +15259,8 @@ void Plater::load_project(wxString const& filename2, BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "filename is: " << filename2 << "and originfile is: " << originfile; BOOST_LOG_TRIVIAL(info) << __FUNCTION__; auto filename = filename2; - auto check = [&filename, this] (bool yes_or_no) { - if (!yes_or_no && !wxGetApp().check_and_save_current_preset_changes(_L("Load project"), - _L("Some presets are modified."))) + auto check = [&filename, this](bool yes_or_no) { + if (!yes_or_no && !wxGetApp().check_and_save_current_preset_changes(_L("Load project"), _L("Some presets are modified."))) return false; if (filename.empty()) { // Ask user for a project file name. @@ -13285,8 +15277,8 @@ void Plater::load_project(wxString const& filename2, // BBS if (m_loading_project) { - //some error cases happens - //return directly + // some error cases happens + // return directly BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": current loading other project, return directly"); return; } @@ -13294,7 +15286,7 @@ void Plater::load_project(wxString const& filename2, m_loading_project = true; ScopeGuard loading_project_sc([this]() { m_loading_project = false; }); // Make sure state restored on any early return - m_only_gcode = false; + m_only_gcode = false; m_exported_file = false; get_notification_manager()->bbl_close_plateinfo_notification(); get_notification_manager()->bbl_close_preview_only_notification(); @@ -13305,7 +15297,7 @@ void Plater::load_project(wxString const& filename2, get_notification_manager()->close_notification_of_type(NotificationType::SlicingSeriousWarning); get_notification_manager()->close_notification_of_type(NotificationType::SlicingWarning); - auto path = into_path(filename); + auto path = into_path(filename); auto strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig; if (originfile == "") { @@ -13316,9 +15308,9 @@ void Plater::load_project(wxString const& filename2, strategy = strategy | LoadStrategy::Restore; } else { switch (determine_load_type(filename.ToStdString())) { - case LoadType::OpenProject: break; // Do nothing - case LoadType::LoadGeometry:; strategy = LoadStrategy::LoadModel; break; - default: return; // User cancelled + case LoadType::OpenProject: break; // Do nothing + case LoadType::LoadGeometry:; strategy = LoadStrategy::LoadModel; break; + default: return; // User cancelled } } bool load_restore = strategy & LoadStrategy::Restore; @@ -13333,7 +15325,7 @@ void Plater::load_project(wxString const& filename2, if (strategy & LoadStrategy::Restore) input_paths.push_back(into_u8(originfile)); - bool loaded_published = false; + bool loaded_published = false; std::vector res = load_files(input_paths, strategy, false, &loaded_published); reset_project_dirty_initial_presets(); @@ -13345,15 +15337,14 @@ void Plater::load_project(wxString const& filename2, BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << (load_restore ? originfile : filename); p->set_project_filename(load_restore ? originfile : filename); if (load_restore && originfile.IsEmpty()) { - p->set_project_name(_L("Untitled")); + p->set_project_name(_L("Untitled")); } } else { if (using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; p->set_project_filename(filename); - } - else if (loaded_published) { + } else if (loaded_published) { // A "published" 3MF loads as a new project: its path must not become the project // filename (Save/Ctrl-S prompts for a destination instead of overwriting it); // reset() already cleared the project name, so restore the default title and keep @@ -13362,17 +15353,15 @@ void Plater::load_project(wxString const& filename2, if (!filename.IsEmpty()) wxGetApp().mainframe->add_to_recent_projects(filename); } - } // BBS set default 3D view and direction after loading project - //p->select_view_3D("3D"); + // p->select_view_3D("3D"); if (!m_exported_file) { p->select_view("topfront"); p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - } - else { + } else { p->partplate_list.select_plate_view(); } @@ -13392,7 +15381,7 @@ void Plater::load_project(wxString const& filename2, wxGetApp().params_panel()->switch_to_object_if_has_object_configs(); - auto has_modify = is_flush_config_modified(); + auto has_modify = is_flush_config_modified(); sidebar().set_flushing_volume_warning(has_modify); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load project done"; @@ -13401,8 +15390,8 @@ void Plater::load_project(wxString const& filename2, // BBS: save logic int Plater::save_project(bool saveAs) { - //if (up_to_date(false, false)) // should we always save - // return; + // if (up_to_date(false, false)) // should we always save + // return; auto filename = get_project_filename(".3mf"); if (!saveAs && filename.IsEmpty()) saveAs = true; @@ -13413,15 +15402,18 @@ int Plater::save_project(bool saveAs) if (filename == "") return wxID_CANCEL; - //BBS export 3mf without gcode - auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh; + // BBS export 3mf without gcode + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh; bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); if (full_pathnames) { save_strategy = save_strategy | SaveStrategy::FullPathSources; } if (export_3mf(into_path(filename), save_strategy) < 0) { - MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."), - _L("Save project"), wxOK | wxICON_WARNING).ShowModal(); + MessageDialog(this, + _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project " + "file open."), + _L("Save project"), wxOK | wxICON_WARNING) + .ShowModal(); return wxID_CANCEL; } @@ -13438,18 +15430,17 @@ int Plater::save_project(bool saveAs) try { json j; boost::uintmax_t size = boost::filesystem::file_size(into_path(filename)); - j["file_size"] = size; - j["file_name"] = std::string(filename.mb_str()); + j["file_size"] = size; + j["file_name"] = std::string(filename.mb_str()); NetworkAgent* agent = wxGetApp().getAgent(); - } - catch (...) {} + } catch (...) {} update_title_dirty_status(); return wxID_YES; } -//BBS import model by model id +// BBS import model by model id void Plater::import_model_id(wxString download_info) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " download info: " << download_info; @@ -13459,195 +15450,175 @@ void Plater::import_model_id(wxString download_info) wxString filename; wxString separator = "&name="; - try - { + try { size_t namePos = download_info.Find(separator); if (namePos != wxString::npos) { download_url = download_info.Mid(0, namePos); - filename = download_info.Mid(namePos + separator.Length()); + filename = download_info.Mid(namePos + separator.Length()); - } - else { + } else { fs::path download_path = fs::path(download_origin_url.wx_str()); - download_url = download_origin_url; - filename = download_path.filename().string(); + download_url = download_origin_url; + filename = download_path.filename().string(); } - } - catch (const std::exception&) - { - //wxString sError = error.what(); + } catch (const std::exception&) { + // wxString sError = error.what(); } - bool download_ok = false; - int retry_count = 0; + bool download_ok = false; + int retry_count = 0; const int max_retries = 3; /* jump to 3D eidtor */ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); /* prepare progress dialog */ - bool cont = true; + bool cont = true; bool cont_dlg = true; - bool cancel = false; + bool cancel = false; wxString msg; wxString dlg_title = _L("Importing Model"); int percent = 0; - ProgressDialog dlg(dlg_title, - wxString(' ', 100) + "\n\n\n\n", - 100, // range - this, // parent - wxPD_CAN_ABORT | - wxPD_APP_MODAL | - wxPD_AUTO_HIDE | - wxPD_SMOOTH); + ProgressDialog dlg(dlg_title, wxString(' ', 100) + "\n\n\n\n", + 100, // range + this, // parent + wxPD_CAN_ABORT | wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH); boost::filesystem::path target_path; - //reset params + // reset params p->project.reset(); /* prepare project and profile */ - boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &cancel, &retry_count, max_retries, &msg, &target_path, &download_ok, download_url, &filename] { + boost::thread import_thread = Slic3r::create_thread( + [&percent, &cont, &cancel, &retry_count, max_retries, &msg, &target_path, &download_ok, download_url, &filename] { + // Orca: NetworkAgent is not needed and only prevents this from running + // NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); + // if (!m_agent) return; - // Orca: NetworkAgent is not needed and only prevents this from running -// NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); -// if (!m_agent) return; + int res = 0; + std::string http_body; - int res = 0; - std::string http_body; + msg = _L("Preparing 3MF file..."); - msg = _L("Preparing 3MF file..."); + // gets the number of files with the same name + std::vector vecFiles; + bool is_already_exist = false; - //gets the number of files with the same name - std::vector vecFiles; - bool is_already_exist = false; + target_path = fs::path(wxGetApp().app_config->get("download_path")); + try { + vecFiles.clear(); + wxString extension = fs::path(filename.wx_str()).extension().c_str(); - target_path = fs::path(wxGetApp().app_config->get("download_path")); - - try - { - vecFiles.clear(); - wxString extension = fs::path(filename.wx_str()).extension().c_str(); - - - //check file suffix - if (!extension.Contains(".3mf")) { - msg = _L("Download failed; unknown file format."); - return; - } - - auto name = filename.substr(0, filename.length() - extension.length() - 1); - - for (const auto& iter : boost::filesystem::directory_iterator(target_path)) - { - if (boost::filesystem::is_directory(iter.path())) - continue; - - wxString sFile = iter.path().filename().string().c_str(); - if (strstr(sFile.c_str(), name.c_str()) != NULL) { - vecFiles.push_back(sFile); + // check file suffix + if (!extension.Contains(".3mf")) { + msg = _L("Download failed; unknown file format."); + return; } - if (sFile == filename) is_already_exist = true; + auto name = filename.substr(0, filename.length() - extension.length() - 1); + + for (const auto& iter : boost::filesystem::directory_iterator(target_path)) { + if (boost::filesystem::is_directory(iter.path())) + continue; + + wxString sFile = iter.path().filename().string().c_str(); + if (strstr(sFile.c_str(), name.c_str()) != NULL) { + vecFiles.push_back(sFile); + } + + if (sFile == filename) + is_already_exist = true; + } + } catch (const std::exception&) { + // wxString sError = error.what(); } - } - catch (const std::exception&) - { - //wxString sError = error.what(); - } - //update filename - if (is_already_exist && vecFiles.size() >= 1) { - wxString extension = fs::path(filename.wx_str()).extension().c_str(); - wxString name = filename.substr(0, filename.length() - extension.length()); - filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString(); - } + // update filename + if (is_already_exist && vecFiles.size() >= 1) { + wxString extension = fs::path(filename.wx_str()).extension().c_str(); + wxString name = filename.substr(0, filename.length() - extension.length()); + filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString(); + } + msg = _L("Downloading project..."); - msg = _L("Downloading project..."); + // target_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); - //target_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); + // target_path = wxGetApp().get_local_models_path().c_str(); + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + std::string unique = to_string(uuid).substr(0, 6); + if (filename.empty()) { + filename = "untitled.3mf"; + } - //target_path = wxGetApp().get_local_models_path().c_str(); - boost::uuids::uuid uuid = boost::uuids::random_generator()(); - std::string unique = to_string(uuid).substr(0, 6); + // target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str(); + target_path /= fs::path(filename.wc_str()); - if (filename.empty()) { - filename = "untitled.3mf"; - } + fs::path tmp_path = target_path; + tmp_path += format(".%1%", ".download"); - //target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str(); - target_path /= fs::path(filename.wc_str()); + auto filesize = 0; + bool size_limit = false; + auto http = Http::get(download_url.ToStdString()); - fs::path tmp_path = target_path; - tmp_path += format(".%1%", ".download"); - - auto filesize = 0; - bool size_limit = false; - auto http = Http::get(download_url.ToStdString()); - - while (cont && retry_count < max_retries) { - retry_count++; - http.on_progress([&percent, &cont, &msg, &filesize, &size_limit](Http::Progress progress, bool& cancel) { - - if (!cont) cancel = true; - if (progress.dltotal != 0) { - - if (filesize == 0) { - filesize = progress.dltotal; - double megabytes = static_cast(progress.dltotal) / (1024 * 1024); - //The maximum size of a 3mf file is 500mb - if (megabytes > 500) { - cont = false; - size_limit = true; + while (cont && retry_count < max_retries) { + retry_count++; + http.on_progress([&percent, &cont, &msg, &filesize, &size_limit](Http::Progress progress, bool& cancel) { + if (!cont) + cancel = true; + if (progress.dltotal != 0) { + if (filesize == 0) { + filesize = progress.dltotal; + double megabytes = static_cast(progress.dltotal) / (1024 * 1024); + // The maximum size of a 3mf file is 500mb + if (megabytes > 500) { + cont = false; + size_limit = true; + } } + percent = progress.dlnow * 100 / progress.dltotal; } - percent = progress.dlnow * 100 / progress.dltotal; - } - if (size_limit) { - msg = _L("Download failed; File size exception."); - } - else { - msg = wxString::Format(_L("Project downloaded %d%%"), percent); - } - }) - .on_error([&msg, &cont, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) { - (void)body; - BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", - body, - http_status, - error); + if (size_limit) { + msg = _L("Download failed; File size exception."); + } else { + msg = wxString::Format(_L("Project downloaded %d%%"), percent); + } + }) + .on_error([&msg, &cont, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) { + (void) body; + BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", body, http_status, error); - if (retry_count == max_retries) { - msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it."); - cont = false; - } - }) - .on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) { + if (retry_count == max_retries) { + msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it."); + cont = false; + } + }) + .on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) { fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); file.write(body.c_str(), body.size()); file.close(); fs::rename(tmp_path, target_path); - cont = false; + cont = false; download_ok = true; - }).perform_sync(); + }) + .perform_sync(); // for break while - //cont = false; - } - - }); + // cont = false; + } + }); while (cont && cont_dlg) { wxMilliSleep(50); cont_dlg = dlg.Update(percent, msg); if (!cont_dlg) { - cont = cont_dlg; + cont = cont_dlg; cancel = true; } @@ -13668,13 +15639,15 @@ void Plater::import_model_id(wxString download_info) BOOST_LOG_TRIVIAL(trace) << "import_model_id: target_path = " << target_path.string(); /* load project */ // Orca: If download is a zip file, treat it as if file has been drag and dropped on the plater - if (target_path.extension() == ".zip") - { wxArrayString arr; arr.Add(from_path(target_path)); this->load_files(arr); } - else + if (target_path.extension() == ".zip") { + wxArrayString arr; + arr.Add(from_path(target_path)); + this->load_files(arr); + } else this->load_project(from_path(target_path)); /*BBS set project info after load project, project info is reset in load project */ - //p->project.project_model_id = model_id; - //p->project.project_design_id = design_id; + // p->project.project_model_id = model_id; + // p->project.project_design_id = design_id; AppConfig* config = wxGetApp().app_config; if (config) { p->project.project_country_code = config->get_country_code(); @@ -13683,8 +15656,7 @@ void Plater::import_model_id(wxString download_info) // show save new project p->set_project_filename(target_path.wstring()); p->notification_manager->push_import_finished_notification(target_path.string(), target_path.parent_path().string(), false); - } - else { + } else { if (!msg.empty()) { MessageDialog msg_wingow(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK); msg_wingow.SetSize(wxSize(FromDIP(480), -1)); @@ -13693,11 +15665,8 @@ void Plater::import_model_id(wxString download_info) return; } } -//BBS download project by project id -void Plater::download_project(const wxString& project_id) -{ - return; -} +// BBS download project by project id +void Plater::download_project(const wxString& project_id) { return; } void Plater::request_model_download(wxString url) { @@ -13720,8 +15689,7 @@ bool Plater::up_to_date(bool saved, bool backup) Slic3r::clear_other_changes(backup); return p->up_to_date(saved, backup); } - return p->model.objects.empty() || (p->up_to_date(saved, backup) && - !Slic3r::has_other_changes(backup)); + return p->model.objects.empty() || (p->up_to_date(saved, backup) && !Slic3r::has_other_changes(backup)); } bool Plater::add_model(bool imperial_units, std::string fname) @@ -13736,13 +15704,12 @@ bool Plater::add_model(bool imperial_units, std::string fname) for (const auto& file : input_files) paths.emplace_back(into_path(file)); - } - else { + } else { paths.emplace_back(fname); } std::string snapshot_label; - assert(! paths.empty()); + assert(!paths.empty()); if (paths.size() == 1) { snapshot_label = "Import Object"; snapshot_label += ": "; @@ -13751,7 +15718,7 @@ bool Plater::add_model(bool imperial_units, std::string fname) snapshot_label = "Import Objects"; snapshot_label += ": "; snapshot_label += paths.front().filename().string().c_str(); - for (size_t i = 1; i < paths.size(); ++ i) { + for (size_t i = 1; i < paths.size(); ++i) { snapshot_label += ", "; snapshot_label += encode_path(paths[i].filename().string().c_str()); } @@ -13763,11 +15730,21 @@ bool Plater::add_model(bool imperial_units, std::string fname) auto loadfiles_type = LoadFilesType::NoFile; auto amf_files_count = get_3mf_file_count(paths); - if (paths.size() > 1 && amf_files_count < paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } - if (paths.size() > 1 && amf_files_count == paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } - if (paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } - if (paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; - if (paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; + if (paths.size() > 1 && amf_files_count < paths.size()) { + loadfiles_type = LoadFilesType::Multiple3MFOther; + } + if (paths.size() > 1 && amf_files_count == paths.size()) { + loadfiles_type = LoadFilesType::Multiple3MF; + } + if (paths.size() > 1 && amf_files_count == 0) { + loadfiles_type = LoadFilesType::MultipleOther; + } + if (paths.size() == 1 && amf_files_count == 1) { + loadfiles_type = LoadFilesType::Single3MF; + }; + if (paths.size() == 1 && amf_files_count == 0) { + loadfiles_type = LoadFilesType::SingleOther; + }; bool ask_multi = false; @@ -13775,10 +15752,10 @@ bool Plater::add_model(bool imperial_units, std::string fname) ask_multi = true; auto strategy = LoadStrategy::LoadModel; - if (imperial_units) strategy = strategy | LoadStrategy::ImperialUnits; + if (imperial_units) + strategy = strategy | LoadStrategy::ImperialUnits; const bool loaded = !load_files(paths, strategy, ask_multi).empty(); if (loaded) { - if (get_project_name() == _L("Untitled") && paths.size() > 0) { boost::filesystem::path full_path(paths[0].string()); p->set_project_name(from_u8(full_path.stem().string())); @@ -13794,22 +15771,16 @@ void Plater::calib_pa(const Calib_Params& params) const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false)); print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); switch (params.mode) { - case CalibMode::Calib_PA_Line: - add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/pressure_advance_test.drc"); - break; - case CalibMode::Calib_PA_Pattern: - _calib_pa_pattern(params); - break; - case CalibMode::Calib_PA_Tower: - _calib_pa_tower(params); - break; - default: break; + case CalibMode::Calib_PA_Line: add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/pressure_advance_test.drc"); break; + case CalibMode::Calib_PA_Pattern: _calib_pa_pattern(params); break; + case CalibMode::Calib_PA_Tower: _calib_pa_tower(params); break; + default: break; } p->background_process.fff_print()->set_calib_params(params); } @@ -13820,17 +15791,17 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) std::vector accels{params.accelerations}; std::vector object_idxs{}; /* Set common parameters */ - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; DynamicPrintConfig& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); set_config_values(filament_config, "filament_retract_when_changing_layer", false); set_config_values(filament_config, "filament_wipe", false); set_config_values(printer_config, "wipe", false); set_config_values(printer_config, "retract_when_changing_layer", false); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool(false)); - //Orca: find acceleration to use in the test + // Orca: find acceleration to use in the test auto accel = print_config.get_abs_value_at("outer_wall_acceleration", params.extruder_id); // get the outer wall acceleration if (accel == 0) // if outer wall accel isnt defined, fall back to inner wall accel accel = print_config.get_abs_value_at("inner_wall_acceleration", params.extruder_id); @@ -13840,26 +15811,27 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) // is set to the travel accel before printing the pattern. if (accels.empty()) { accels.assign({accel}); - const auto msg{_L("INFO:") + "\n" + - _L("No accelerations provided for calibration. Use default acceleration value ") + std::to_string(long(accel)) + _L(u8"mm/s²")}; + const auto msg{_L("INFO:") + "\n" + _L("No accelerations provided for calibration. Use default acceleration value ") + + std::to_string(long(accel)) + _L(u8"mm/s²")}; get_notification_manager()->push_notification(msg.utf8_string()); } else { // set max acceleration in case of batch mode to get correct test pattern size accel = *std::max_element(accels.begin(), accels.end()); } set_config_values(&print_config, "outer_wall_acceleration", accel); - print_config.set_key_value( "print_sequence", new ConfigOptionEnum(PrintSequence::ByLayer)); - - //Orca: find jerk value to use in the test - if(!has_junction_deviation(printer_config) && print_config.get_abs_value_at("default_jerk", params.extruder_id) > 0){ // we have set a jerk value + print_config.set_key_value("print_sequence", new ConfigOptionEnum(PrintSequence::ByLayer)); + + // Orca: find jerk value to use in the test + if (!has_junction_deviation(printer_config) && + print_config.get_abs_value_at("default_jerk", params.extruder_id) > 0) { // we have set a jerk value auto jerk = print_config.get_abs_value_at("outer_wall_jerk", params.extruder_id); // get outer wall jerk if (jerk == 0) // if outer wall jerk is not defined, get inner wall jerk jerk = print_config.get_abs_value_at("inner_wall_jerk", params.extruder_id); if (jerk == 0) // if inner wall jerk is not defined, get the default jerk jerk = print_config.get_abs_value_at("default_jerk", params.extruder_id); - - //Orca: Set jerk values. Again first layer jerk should not matter as it is reset to the travel jerk before the - // first PA pattern is printed. + + // Orca: Set jerk values. Again first layer jerk should not matter as it is reset to the travel jerk before the + // first PA pattern is printed. set_config_values(&print_config, "default_jerk", jerk); set_config_values(&print_config, "outer_wall_jerk", jerk); set_config_values(&print_config, "inner_wall_jerk", jerk); @@ -13868,45 +15840,38 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) set_config_values(&print_config, "travel_jerk", jerk); } - if (has_junction_deviation(printer_config)){ + if (has_junction_deviation(printer_config)) { set_config_values(&print_config, "default_junction_deviation", 0); } - + for (const auto& opt : SuggestedConfigCalibPAPattern().floats_pairs) { set_config_values(&print_config, opt.first, opt.second[0]); } for (const auto& opt : SuggestedConfigCalibPAPattern().nozzle_ratio_pairs) { - print_config.set_key_value( - opt.first, - new ConfigOptionFloatOrPercent(nozzle_diameter * opt.second / 100, false) - ); + print_config.set_key_value(opt.first, new ConfigOptionFloatOrPercent(nozzle_diameter * opt.second / 100, false)); } for (const auto& opt : SuggestedConfigCalibPAPattern().int_pairs) { - print_config.set_key_value( - opt.first, - new ConfigOptionInt(opt.second) - ); + print_config.set_key_value(opt.first, new ConfigOptionInt(opt.second)); } print_config.set_key_value(SuggestedConfigCalibPAPattern().brim_pair.first, - new ConfigOptionEnum(SuggestedConfigCalibPAPattern().brim_pair.second)); + new ConfigOptionEnum(SuggestedConfigCalibPAPattern().brim_pair.second)); print_config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); // Orca: Set the outer wall speed to the optimal speed for the test, cap it with max volumetric speed if (speeds.empty()) { // TODO: per-variant cap - double speed = CalibPressureAdvance::find_optimal_PA_speed( - wxGetApp().preset_bundle->full_config(), - print_config.get_abs_value("line_width", nozzle_diameter), - print_config.get_abs_value("layer_height"), 0, 0); + double speed = CalibPressureAdvance::find_optimal_PA_speed(wxGetApp().preset_bundle->full_config(), + print_config.get_abs_value("line_width", nozzle_diameter), + print_config.get_abs_value("layer_height"), 0, 0); set_config_values(&print_config, "outer_wall_speed", speed); speeds.assign({speed}); - const auto msg{_L("INFO:") + "\n" + - _L("No speeds provided for calibration. Use default optimal speed ") + std::to_string(long(speed)) + _L("mm/s")}; + const auto msg{_L("INFO:") + "\n" + _L("No speeds provided for calibration. Use default optimal speed ") + + std::to_string(long(speed)) + _L("mm/s")}; get_notification_manager()->push_notification(msg.utf8_string()); } else if (speeds.size() == 1) { // If we have single value provided, set speed using global configuration. @@ -13922,21 +15887,15 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - const bool is_bbl_machine = preset_bundle->is_bbl_vendor(); - auto cur_plate = get_partplate_list().get_plate(0); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + const bool is_bbl_machine = preset_bundle->is_bbl_vendor(); + auto cur_plate = get_partplate_list().get_plate(0); // add "handle" cube sidebar().obj_list()->load_generic_subobject("Cube", ModelVolumeType::INVALID); - auto *cube = model().objects[0]; + auto* cube = model().objects[0]; - CalibPressureAdvancePattern pa_pattern( - params, - full_config, - is_bbl_machine, - *cube, - cur_plate->get_origin() - ); + CalibPressureAdvancePattern pa_pattern(params, full_config, is_bbl_machine, *cube, cur_plate->get_origin()); /* Having PA pattern configured, we could make a set of polygons resembling N test patterns. * We'll arrange this set of polygons, so we would know position of each test pattern and @@ -13946,8 +15905,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) * as a reference for objects arrangement. Polygon is slightly oversized to add spaces between patterns. * That arrangement will be used to place 'handle cubes' for each test. */ auto cube_bb = cube->raw_bounding_box(); - cube->scale((pa_pattern.print_size_x() + 4) / cube_bb.size().x(), - (pa_pattern.print_size_y() + 4) / cube_bb.size().y(), + cube->scale((pa_pattern.print_size_x() + 4) / cube_bb.size().x(), (pa_pattern.print_size_y() + 4) / cube_bb.size().y(), pa_pattern.max_layer_z() / cube_bb.size().z()); arrangement::ArrangePolygons arranged_items; @@ -13955,7 +15913,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) arrangement::ArrangeParams ap; Points bedpts = arrangement::get_shrink_bedpts(&full_config, ap); - for(size_t i = 0; i < speeds.size() * accels.size(); i++) { + for (size_t i = 0; i < speeds.size() * accels.size(); i++) { arrangement::ArrangePolygon p; cube->instances[0]->get_arrange_polygon(&p); p.bed_idx = 0; @@ -13967,24 +15925,23 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) /* scale cube back to the size of test pattern 'handle' */ cube_bb = cube->raw_bounding_box(); - cube->scale(pa_pattern.handle_xy_size() / cube_bb.size().x(), - pa_pattern.handle_xy_size() / cube_bb.size().y(), + cube->scale(pa_pattern.handle_xy_size() / cube_bb.size().x(), pa_pattern.handle_xy_size() / cube_bb.size().y(), pa_pattern.max_layer_z() / cube_bb.size().z()); /* Set speed and acceleration on per-object basis and arrange anchor object on the plates. * Test gcode will be genecated during plate slicing */ - for(size_t test_idx = 0; test_idx < arranged_items.size(); test_idx++) { - const auto &ai = arranged_items[test_idx]; + for (size_t test_idx = 0; test_idx < arranged_items.size(); test_idx++) { + const auto& ai = arranged_items[test_idx]; size_t plate_idx = arranged_items[test_idx].bed_idx; - auto tspd = speeds[test_idx % speeds.size()]; - auto tacc = accels[test_idx / speeds.size()]; + auto tspd = speeds[test_idx % speeds.size()]; + auto tacc = accels[test_idx / speeds.size()]; /* make an own copy of anchor cube for each test */ - auto obj = test_idx == 0 ? cube : model().add_object(*cube); + auto obj = test_idx == 0 ? cube : model().add_object(*cube); auto obj_idx = std::distance(model().objects.begin(), std::find(model().objects.begin(), model().objects.end(), obj)); obj->name.assign(std::string("pa_pattern_") + std::to_string(int(tspd)) + std::string("_") + std::to_string(int(tacc))); - auto &obj_config = obj->config; + auto& obj_config = obj->config; if (speeds.size() > 1) obj_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable(1, tspd)); if (accels.size() > 1) @@ -13998,9 +15955,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) object_idxs.emplace_back(obj_idx); get_partplate_list().add_to_plate(obj_idx, 0, plate_idx); - const Vec3d obj_offset{unscale(ai.translation(X)), - unscale(ai.translation(Y)), - 0}; + const Vec3d obj_offset{unscale(ai.translation(X)), unscale(ai.translation(Y)), 0}; obj->instances[0]->set_offset(cur_plate->get_origin() + obj_offset + pa_pattern.handle_pos_offset()); obj->ensure_on_bed(); @@ -14027,27 +15982,23 @@ void Plater::_calib_pa_pattern_gen_gcode() * We'll store gcode for all tests on a single plate here. Once the plate handling is done, * all the g-codes will be merged into a single one on per-layer basis */ std::vector mgc; - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; /* iterate over all cubes on current plate and generate gcode for them */ for (auto obj : cur_plate->get_objects_on_this_plate()) { - auto gcode = model().calib_pa_pattern->generate_custom_gcodes( - preset_bundle->full_config(), - preset_bundle->is_bbl_vendor(), - *obj, - cur_plate->get_origin() - ); + auto gcode = model().calib_pa_pattern->generate_custom_gcodes(preset_bundle->full_config(), preset_bundle->is_bbl_vendor(), *obj, + cur_plate->get_origin()); mgc.emplace_back(gcode); } // move first item into model custom gcode - auto &pcgc = model().plates_custom_gcodes[get_partplate_list().get_curr_plate_index()]; - pcgc = std::move(mgc[0]); + auto& pcgc = model().plates_custom_gcodes[get_partplate_list().get_curr_plate_index()]; + pcgc = std::move(mgc[0]); mgc.erase(mgc.begin()); // concat layer gcodes for each test for (size_t i = 0; i < pcgc.gcodes.size(); i++) { - for (auto &gc : mgc) { + for (auto& gc : mgc) { pcgc.gcodes[i].extra += gc.gcodes[i].extra; } } @@ -14056,35 +16007,35 @@ void Plater::_calib_pa_pattern_gen_gcode() void Plater::cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes) { wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto *object = p->model.objects[obj_idx]; + auto* object = p->model.objects[obj_idx]; wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) + if (!attributes.has(ModelObjectCutAttribute::KeepUpper) && !attributes.has(ModelObjectCutAttribute::KeepLower)) return; wxBusyCursor wait; const Vec3d instance_offset = object->instances[instance_idx]->get_offset(); - Cut cut(object, instance_idx, Geometry::translation_transform(z * Vec3d::UnitZ() - instance_offset), attributes); - const auto new_objects = cut.perform_with_plane(); + Cut cut(object, instance_idx, Geometry::translation_transform(z * Vec3d::UnitZ() - instance_offset), attributes); + const auto new_objects = cut.perform_with_plane(); apply_cut_object_to_model(obj_idx, new_objects); } -void Plater::_calib_pa_tower(const Calib_Params& params) { +void Plater::_calib_pa_tower(const Calib_Params& params) +{ if (!add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/tower_with_seam.drc")) return; - auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; const double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); print_config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{ 1.0f }); - + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{1.0f}); auto& obj_cfg = model().objects[0]->config; @@ -14104,7 +16055,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) { obj_cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); print_config.set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); @@ -14113,7 +16064,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) { wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); auto new_height = std::ceil((params.end - params.start) / params.step) + 1; - auto obj_bb = model().objects[0]->bounding_box_exact(); + auto obj_bb = model().objects[0]->bounding_box_exact(); if (new_height < obj_bb.size().z()) { cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); } @@ -14121,7 +16072,8 @@ void Plater::_calib_pa_tower(const Calib_Params& params) { _calib_pa_select_added_objects(); } -void Plater::_calib_pa_select_added_objects() { +void Plater::_calib_pa_select_added_objects() +{ // update printable state for new volumes on canvas3D wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_objects({0}); @@ -14143,27 +16095,28 @@ void Plater::_calib_pa_select_added_objects() { // ORCA: Add pattern parameter void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, int pass, InfillPattern pattern) { - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; /// --- scale --- // model is created for a 0.4 nozzle, scale z with nozzle size. const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); - std::vector extruder_types = printer_config->option("extruder_type")->values; - std::vector nozzle_volume_types = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; + std::vector extruder_types = printer_config->option("extruder_type")->values; + std::vector nozzle_volume_types = + wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; assert(nozzle_diameter_config->values.size() > 0); float nozzle_diameter = nozzle_diameter_config->values[0]; - float xyScale = nozzle_diameter / 0.6; - //scale z to have 10 layers - // 2 bottom, 5 top, 3 sparse infill + float xyScale = nozzle_diameter / 0.6; + // scale z to have 10 layers + // 2 bottom, 5 top, 3 sparse infill double first_layer_height = print_config->option("initial_layer_print_height")->value; - double layer_height = nozzle_diameter / 2.0; // prefer 0.2 layer height for 0.4 nozzle - first_layer_height = std::max(first_layer_height, layer_height); + double layer_height = nozzle_diameter / 2.0; // prefer 0.2 layer height for 0.4 nozzle + first_layer_height = std::max(first_layer_height, layer_height); - const auto canvas = wxGetApp().plater()->canvas3D(); - auto& selection = canvas->get_selection(); + const auto canvas = wxGetApp().plater()->canvas3D(); + auto& selection = canvas->get_selection(); selection.setup_cache(); TransformationType transformation_type; transformation_type.set_relative(); @@ -14178,7 +16131,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i auto cur_flowrate = filament_config->option("filament_flow_ratio")->get_at(0); // TODO: per-filament param std::vector internal_solid_speeds = generate_max_speed_parameter_value("internal_solid_infill_speed", linear, pass); - std::vector top_surface_speeds = generate_max_speed_parameter_value("top_surface_speed", linear, pass); + std::vector top_surface_speeds = generate_max_speed_parameter_value("top_surface_speed", linear, pass); // adjust parameters for (auto _obj : objects) { @@ -14189,7 +16142,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i _obj->config.set_key_value("enable_extra_bridge_layer", new ConfigOptionEnum(eblDisabled)); _obj->config.set_key_value("internal_bridge_density", new ConfigOptionPercent(100)); _obj->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(35)); - _obj->config.set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(100,true)); + _obj->config.set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(100, true)); _obj->config.set_key_value("bottom_shell_layers", new ConfigOptionInt(2)); _obj->config.set_key_value("top_shell_layers", new ConfigOptionInt(5)); _obj->config.set_key_value("top_shell_thickness", new ConfigOptionFloat(0)); @@ -14204,7 +16157,8 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i _obj->config.set_key_value("top_solid_infill_flow_ratio", new ConfigOptionFloat(1.0f)); _obj->config.set_key_value("infill_direction", new ConfigOptionFloat(45)); _obj->config.set_key_value("solid_infill_direction", new ConfigOptionFloat(135)); - _obj->config.set_key_value("center_of_surface_pattern", new ConfigOptionEnum(CenterOfSurfacePattern::Each_Surface)); + _obj->config.set_key_value("center_of_surface_pattern", + new ConfigOptionEnum(CenterOfSurfacePattern::Each_Surface)); _obj->config.set_key_value("separated_infills", new ConfigOptionBool(false)); _obj->config.set_key_value("align_infill_direction_to_model", new ConfigOptionBool(true)); _obj->config.set_key_value("ironing_type", new ConfigOptionEnum(IroningType::NoIroning)); @@ -14229,20 +16183,18 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i obj_name[0] = '-'; // Orca: force set locale to C to avoid parsing error const std::string _loc = std::setlocale(LC_NUMERIC, nullptr); - std::setlocale(LC_NUMERIC,"C"); - auto modifier = 1.0f; + std::setlocale(LC_NUMERIC, "C"); + auto modifier = 1.0f; try { modifier = stof(obj_name); - } catch (...) { - } + } catch (...) {} // restore locale std::setlocale(LC_NUMERIC, _loc.c_str()); - if(linear) - _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat((cur_flowrate + modifier)/cur_flowrate)); + if (linear) + _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat((cur_flowrate + modifier) / cur_flowrate)); else - _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat(1.0f + modifier/100.f)); - + _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat(1.0f + modifier / 100.f)); } print_config->set_key_value("layer_height", new ConfigOptionFloat(layer_height)); @@ -14260,7 +16212,8 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i } // ORCA: Add pattern parameter -void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { +void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) +{ if (pass != 1 && pass != 2) return; wxString calib_name; @@ -14303,27 +16256,28 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { changed_objects(object_idx); } - -void Plater::calib_temp(const Calib_Params& params) { +void Plater::calib_temp(const Calib_Params& params) +{ constexpr double base_temp_tower_nozzle_diameter = 0.4; - constexpr double base_temp_tower_block_height = 10.0; - constexpr int base_temp_tower_temp_step = 5; + constexpr double base_temp_tower_block_height = 10.0; + constexpr int base_temp_tower_temp_step = 5; const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); new_project(false, false, calib_temp_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - if (params.mode != CalibMode::Calib_Temp_Tower) return; - + if (params.mode != CalibMode::Calib_Temp_Tower) + return; + if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) return; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto start_temp = lround(params.start); + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto start_temp = lround(params.start); const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); - size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); - double nozzle_diameter = base_temp_tower_nozzle_diameter; + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = base_temp_tower_nozzle_diameter; if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { - nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; } if (nozzle_diameter <= 0.0) @@ -14333,7 +16287,7 @@ void Plater::calib_temp(const Calib_Params& params) { const double block_height = base_temp_tower_block_height; // cut upper - auto obj_bb = model().objects[0]->bounding_box_exact(); + auto obj_bb = model().objects[0]->bounding_box_exact(); auto block_count = lround((500 - params.end) / base_temp_tower_temp_step + 1); if (block_count > 0) { // subtract EPSILON offset to avoid cutting at the exact location where the flat surface is @@ -14344,7 +16298,7 @@ void Plater::calib_temp(const Calib_Params& params) { } // cut bottom - obj_bb = model().objects[0]->bounding_box_exact(); + obj_bb = model().objects[0]->bounding_box_exact(); block_count = lround((500 - params.start) / base_temp_tower_temp_step); if (block_count > 0) { auto new_height = block_count * block_height + EPSILON; @@ -14363,7 +16317,7 @@ void Plater::calib_temp(const Calib_Params& params) { set_config_values(filament_config, "nozzle_temperature", (int) start_temp); // When resizing is disabled the 0.4 mm / 0.2 mm reference model is printed as-is (preset layer height kept). if (params.nozzle_based_resize) - model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter/2)); + model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter / 2)); model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(5.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); @@ -14375,10 +16329,9 @@ void Plater::calib_temp(const Calib_Params& params) { auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); if (params.nozzle_based_resize) - print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter/2)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter / 2)); - - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); @@ -14397,23 +16350,23 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc")) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto obj = model().objects[0]; - auto& obj_cfg = obj->config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto obj = model().objects[0]; + auto& obj_cfg = obj->config; - auto bed_shape = printer_config->option("printable_area")->values; + auto bed_shape = printer_config->option("printable_area")->values; BoundingBoxf bed_ext = get_extents(bed_shape); - auto scale_obj = (bed_ext.size().x() - 10) / obj->bounding_box_exact().size().x(); + auto scale_obj = (bed_ext.size().x() - 10) / obj->bounding_box_exact().size().x(); if (scale_obj < 1.0) obj->scale(scale_obj, 1, 1); const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); assert(nozzle_diameter_config->values.size() > 0); double nozzle_diameter = nozzle_diameter_config->values[0]; - double line_width = nozzle_diameter * 1.75; - double layer_height = nozzle_diameter * 0.8; + double line_width = nozzle_diameter * 1.75; + double layer_height = nozzle_diameter * 0.8; auto max_lh = printer_config->option("max_layer_height"); for (size_t i = 0; i < max_lh->values.size(); ++i) { @@ -14422,7 +16375,8 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) } const double filament_max_volumetric_speed = filament_config->option("filament_max_volumetric_speed")->get_at(0); - set_config_values(filament_config, "filament_max_volumetric_speed", std::max(filament_max_volumetric_speed, 200.0)); + set_config_values(filament_config, "filament_max_volumetric_speed", + std::max(filament_max_volumetric_speed, 200.0)); filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); obj_cfg.set_key_value("enable_overhang_speed", new ConfigOptionBoolsNullable(1, false)); @@ -14442,7 +16396,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); @@ -14458,7 +16412,8 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) } auto new_params = params; - auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * filament_config->option("filament_flow_ratio")->get_at(0); + auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * + filament_config->option("filament_flow_ratio")->get_at(0); new_params.end = params.end / mm3_per_mm; new_params.start = params.start / mm3_per_mm; new_params.step = params.step / mm3_per_mm; @@ -14477,10 +16432,10 @@ void Plater::calib_retraction(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + "/calib/retraction/retraction_tower.drc")) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto obj = model().objects[0]; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto obj = model().objects[0]; print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); @@ -14496,7 +16451,8 @@ void Plater::calib_retraction(const Calib_Params& params) auto max_lh = printer_config->option("max_layer_height"); for (size_t i = 0; i < max_lh->values.size(); ++i) { - if (max_lh->values[i] < layer_height) max_lh->values[i] = layer_height; + if (max_lh->values[i] < layer_height) + max_lh->values[i] = layer_height; } printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); @@ -14513,8 +16469,7 @@ void Plater::calib_retraction(const Calib_Params& params) obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); - - changed_objects({ 0 }); + changed_objects({0}); // cut upper auto obj_bb = obj->bounding_box_exact(); @@ -14536,15 +16491,15 @@ void Plater::calib_VFA(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + "/calib/vfa/vfa.drc")) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); - size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); - double nozzle_diameter = vfa_base_nozzle_diameter; + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = vfa_base_nozzle_diameter; if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { - nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; } if (nozzle_diameter <= 0.0) @@ -14576,7 +16531,7 @@ void Plater::calib_VFA(const Calib_Params& params) model().objects[0]->ensure_on_bed(); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); set_config_values(print_config, "enable_overhang_speed", false); print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); @@ -14596,7 +16551,7 @@ void Plater::calib_VFA(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -14604,7 +16559,7 @@ void Plater::calib_VFA(const Calib_Params& params) // Pass the resolved layer height on (only meaningful when resized). GCode's VFA stepping is layer-based, so // it does not require it, but keep it consistent with the geometry. - Calib_Params calib_params = params; + Calib_Params calib_params = params; calib_params.vfa_layer_height = params.nozzle_based_resize ? layer_height : 0.0; p->background_process.fff_print()->set_calib_params(calib_params); } @@ -14617,20 +16572,31 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) if (params.mode != CalibMode::Calib_Input_shaping_freq) return; - if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"))) + if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : + "/calib/input_shaping/fast_tower_test.drc"))) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; const auto gcode_flavor_option = printer_config->option>("gcode_flavor"); if (has_junction_deviation(printer_config)) { - printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats {(std::max(printer_config->option("machine_max_junction_deviation")->values.front(), 0.25))}); + printer_config + ->set_key_value("machine_max_junction_deviation", + new ConfigOptionFloats{ + (std::max(printer_config->option("machine_max_junction_deviation")->values.front(), + 0.25))}); set_config_values(print_config, "default_junction_deviation", 0); } else { const double jerk_value = (gcode_flavor_option && gcode_flavor_option->value == GCodeFlavor::gcfKlipper) ? 5.0 : 10.0; - printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_x")->values.front(), jerk_value)}); - printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_y")->values.front(), jerk_value)}); + printer_config->set_key_value("machine_max_jerk_x", + new ConfigOptionFloats{ + std::max(printer_config->option("machine_max_jerk_x")->values.front(), + jerk_value)}); + printer_config->set_key_value("machine_max_jerk_y", + new ConfigOptionFloats{ + std::max(printer_config->option("machine_max_jerk_y")->values.front(), + jerk_value)}); set_config_values(print_config, "default_jerk", 0); } @@ -14642,8 +16608,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); - filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats{0.0}); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2)); set_config_values(print_config, "enable_overhang_speed", false); @@ -14656,7 +16622,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); - const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), printer_config->option("machine_max_speed_y")->get_at(0)); + const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), + printer_config->option("machine_max_speed_y")->get_at(0)); const double machine_max_acceleration = printer_config->option("machine_max_acceleration_extruding")->get_at(0); set_config_values(print_config, "outer_wall_speed", machine_max_speed); set_config_values(print_config, "default_acceleration", machine_max_acceleration); @@ -14666,7 +16633,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -14683,20 +16650,31 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) if (params.mode != CalibMode::Calib_Input_shaping_damp) return; - if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"))) + if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : + "/calib/input_shaping/fast_tower_test.drc"))) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; const auto gcode_flavor_option = printer_config->option>("gcode_flavor"); if (has_junction_deviation(printer_config)) { - printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats {(std::max(printer_config->option("machine_max_junction_deviation")->values.front(), 0.25))}); + printer_config + ->set_key_value("machine_max_junction_deviation", + new ConfigOptionFloats{ + (std::max(printer_config->option("machine_max_junction_deviation")->values.front(), + 0.25))}); set_config_values(print_config, "default_junction_deviation", 0); } else { const double jerk_value = (gcode_flavor_option && gcode_flavor_option->value == GCodeFlavor::gcfKlipper) ? 5.0 : 10.0; - printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_x")->values.front(), jerk_value)}); - printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_y")->values.front(), jerk_value)}); + printer_config->set_key_value("machine_max_jerk_x", + new ConfigOptionFloats{ + std::max(printer_config->option("machine_max_jerk_x")->values.front(), + jerk_value)}); + printer_config->set_key_value("machine_max_jerk_y", + new ConfigOptionFloats{ + std::max(printer_config->option("machine_max_jerk_y")->values.front(), + jerk_value)}); set_config_values(print_config, "default_jerk", 0); } @@ -14708,8 +16686,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); - filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats{0.0}); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); set_config_values(print_config, "enable_overhang_speed", false); print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); @@ -14721,7 +16699,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); - const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), printer_config->option("machine_max_speed_y")->get_at(0)); + const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), + printer_config->option("machine_max_speed_y")->get_at(0)); const double machine_max_acceleration = printer_config->option("machine_max_acceleration_extruding")->get_at(0); set_config_values(print_config, "outer_wall_speed", machine_max_speed); set_config_values(print_config, "default_acceleration", machine_max_acceleration); @@ -14731,7 +16710,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -14748,12 +16727,12 @@ void Plater::Calib_Cornering(const Calib_Params& params) if (params.mode != CalibMode::Calib_Cornering) return; - const std::string cornering_model_path = params.test_model == 0 - ? "/calib/input_shaping/ringing_tower.drc" - : (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc"); + const std::string cornering_model_path = params.test_model == 0 ? "/calib/input_shaping/ringing_tower.drc" : + (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : + "/calib/cornering/SCV-V2.drc"); if (!add_model(false, Slic3r::resources_dir() + cornering_model_path)) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -14775,8 +16754,8 @@ void Plater::Calib_Cornering(const Calib_Params& params) printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true}); printer_config->set_key_value("input_shaping_type", new ConfigOptionEnum(InputShaperType::Disable)); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); - filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats{0.0}); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); const double filament_max_volumetric_speed = filament_config->option("filament_max_volumetric_speed")->get_at(0); filament_config->set_key_value("filament_max_volumetric_speed", new ConfigOptionFloats{std::max(filament_max_volumetric_speed, 200.0)}); @@ -14790,7 +16769,8 @@ void Plater::Calib_Cornering(const Calib_Params& params) print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); - const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), printer_config->option("machine_max_speed_y")->get_at(0)); + const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), + printer_config->option("machine_max_speed_y")->get_at(0)); const double machine_max_acceleration = printer_config->option("machine_max_acceleration_extruding")->get_at(0); set_config_values(print_config, "outer_wall_speed", machine_max_speed); set_config_values(print_config, "default_acceleration", machine_max_acceleration); @@ -14800,7 +16780,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({ 0 }); + changed_objects({0}); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -14825,7 +16805,7 @@ void Plater::import_zip_archive() void Plater::import_sl1_archive() { - auto &w = get_ui_job_worker(); + auto& w = get_ui_job_worker(); if (w.is_idle() && p->m_sla_import_dlg->ShowModal() == wxID_OK) { p->take_snapshot(_u8L("Import SLA archive")); replace_job(w, std::make_unique(p->m_sla_import_dlg)); @@ -14837,8 +16817,8 @@ void Plater::extract_config_from_project() wxString input_file; wxGetApp().load_project(this, input_file); - if (! input_file.empty()) - load_files({ into_path(input_file) }, LoadStrategy::LoadConfig); + if (!input_file.empty()) + load_files({into_path(input_file)}, LoadStrategy::LoadConfig); } void Plater::load_gcode() @@ -14850,14 +16830,12 @@ void Plater::load_gcode() load_gcode(input_file); } -//BBS: remove GCodeViewer as seperate APP logic +// BBS: remove GCodeViewer as seperate APP logic void Plater::load_gcode(const wxString& filename) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " entry and filename: " << filename; BOOST_LOG_TRIVIAL(info) << __FUNCTION__; - if (! is_gcode_file(into_u8(filename)) - || (m_last_loaded_gcode == filename && m_only_gcode) - ) + if (!is_gcode_file(into_u8(filename)) || (m_last_loaded_gcode == filename && m_only_gcode)) return; m_last_loaded_gcode = filename; @@ -14869,40 +16847,37 @@ void Plater::load_gcode(const wxString& filename) m_only_gcode = true; // cleanup view before to start loading/processing - //BBS: update gcode to current partplate's + // BBS: update gcode to current partplate's GCodeProcessorResult* current_result = p->partplate_list.get_current_slice_result(); - Print& current_print = p->partplate_list.get_current_fff_print(); - //BBS:already reset in new_project - //current_result->reset(); - //p->gcode_result.reset(); - //reset_gcode_toolpaths(); + Print& current_print = p->partplate_list.get_current_fff_print(); + // BBS:already reset in new_project + // current_result->reset(); + // p->gcode_result.reset(); + // reset_gcode_toolpaths(); p->preview->reload_print(m_only_gcode); wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); p->set_current_panel(p->preview, true); p->get_current_canvas3D()->render(); - //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); + // p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); wxBusyCursor wait; // process gcode GCodeProcessor processor; processor.init_filament_maps_and_nozzle_type_when_import_only_gcode(); - try - { + try { GCodeProcessor::s_IsBBLPrinter = wxGetApp().preset_bundle->is_bbl_vendor(); processor.process_file(filename.ToUTF8().data()); - } - catch (const std::exception& ex) - { + } catch (const std::exception& ex) { show_error(this, ex.what()); return; } *current_result = std::move(processor.extract_result()); - //current_result->filename = filename; + // current_result->filename = filename; BedType bed_type = current_result->bed_type; if (bed_type != BedType::btCount) { - DynamicPrintConfig &proj_config = wxGetApp().preset_bundle->project_config; + DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; proj_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); on_bed_type_change(bed_type); } @@ -14911,14 +16886,14 @@ void Plater::load_gcode(const wxString& filename) current_print.apply_config_for_render(processor.export_config_for_render()); - //BBS: add cost info when drag in gcode - auto& ps = current_result->print_statistics; + // BBS: add cost info when drag in gcode + auto& ps = current_result->print_statistics; double total_cost = 0.0; for (auto volume : ps.total_volumes_per_extruder) { size_t extruder_id = volume.first; - double density = current_result->filament_densities.at(extruder_id); - double cost = current_result->filament_costs.at(extruder_id); - double weight = volume.second * density * 0.001; + double density = current_result->filament_densities.at(extruder_id); + double cost = current_result->filament_costs.at(extruder_id); + double weight = volume.second * density * 0.001; total_cost += weight * cost * 0.001; } current_print.print_statistics().total_cost = total_cost; @@ -14927,13 +16902,15 @@ void Plater::load_gcode(const wxString& filename) // show results p->preview->reload_print(m_only_gcode); - //BBS: zoom to bed 0 for gcode preview - //p->preview->get_canvas3d()->zoom_to_gcode(); + // BBS: zoom to bed 0 for gcode preview + // p->preview->get_canvas3d()->zoom_to_gcode(); p->preview->get_canvas3d()->zoom_to_plate(0); if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) { MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("Does not contain valid G-code."), - wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal(); + wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), + wxCLOSE | wxICON_WARNING | wxCENTRE) + .ShowModal(); set_project_filename(DEFAULT_PROJECT_NAME); } else { set_project_filename(filename); @@ -14944,7 +16921,7 @@ void Plater::load_gcode(const wxString& filename) p->view3D->get_canvas3d()->remove_raycasters_for_picking(SceneRaycaster::EType::Bed); } - p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, false); //20250416 ban gcode to send print + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, false); // 20250416 ban gcode to send print } void Plater::reload_gcode_from_disk() @@ -14954,45 +16931,43 @@ void Plater::reload_gcode_from_disk() load_gcode(filename); } -void Plater::reload_print() -{ - p->preview->reload_print(); -} +void Plater::reload_print() { p->preview->reload_print(); } // BBS -wxString Plater::get_project_name() -{ - return p->get_project_name(); -} +wxString Plater::get_project_name() { return p->get_project_name(); } void Plater::update_all_plate_thumbnails(bool force_update) { for (int i = 0; i < get_partplate_list().get_plate_count(); i++) { - PartPlate* plate = get_partplate_list().get_plate(i); - ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i}; + PartPlate* plate = get_partplate_list().get_plate(i); + ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; if (force_update || !plate->thumbnail_data.is_valid()) { - get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho); + get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, + thumbnail_params, Camera::EType::Ortho); } if (force_update || !plate->no_light_thumbnail_data.is_valid()) { - get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, - Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); + get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, + plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho, + Camera::ViewAngleType::Iso, false, true); } } } -void Plater::update_obj_preview_thumbnail(ModelObject *mo, int obj_idx, int vol_idx, std::vector colors, int camera_view_angle_type) +void Plater::update_obj_preview_thumbnail( + ModelObject* mo, int obj_idx, int vol_idx, std::vector colors, int camera_view_angle_type) { - PartPlate * plate = get_partplate_list().get_plate(0); + PartPlate* plate = get_partplate_list().get_plate(0); ThumbnailsParams thumbnail_params = {{}, false, true, true, true, 0, false}; GLVolumeCollection cur_volumes; cur_volumes.load_object_volume(mo, obj_idx, vol_idx, 0, "volume", true, false, false, false); ModelObjectPtrs model_objects; model_objects.emplace_back(mo); - get_view3D_canvas3D()->render_thumbnail(plate->obj_preview_thumbnail_data, colors, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, - model_objects, cur_volumes, Camera::EType::Ortho, (Camera::ViewAngleType) camera_view_angle_type, false, false); + get_view3D_canvas3D()->render_thumbnail(plate->obj_preview_thumbnail_data, colors, plate->plate_thumbnail_width, + plate->plate_thumbnail_height, thumbnail_params, model_objects, cur_volumes, + Camera::EType::Ortho, (Camera::ViewAngleType) camera_view_angle_type, false, false); } -//invalid all plate's thumbnails +// invalid all plate's thumbnails void Plater::invalid_all_plate_thumbnails() { if (using_exported_file() || skip_thumbnail_invalid) @@ -15009,39 +16984,28 @@ void Plater::invalid_all_plate_thumbnails() void Plater::force_update_all_plate_thumbnails() { if (using_exported_file() || skip_thumbnail_invalid) { - } - else { + } else { invalid_all_plate_thumbnails(); update_all_plate_thumbnails(true); } } // BBS: backup -std::vector Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) { - //BBS: wish to reset state when load a new file +std::vector Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) +{ + // BBS: wish to reset state when load a new file p->m_slice_all_only_has_gcode = false; - //BBS: wish to reset all plates stats item selected state when load a new file + // BBS: wish to reset all plates stats item selected state when load a new file p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); return p->load_files(input_files, strategy, ask_multi, published_out); } -// To be called when providing a list of files to the GUI slic3r on command line. -std::vector Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi) -{ - std::vector paths; - paths.reserve(input_files.size()); - for (const std::string& path : input_files) - paths.emplace_back(path); - return p->load_files(paths, strategy, ask_multi); -} - bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) { - //std::vector unzipped_paths; + // std::vector unzipped_paths; std::vector non_project_paths; std::vector project_paths; - try - { + try { mz_zip_archive archive; mz_zip_zero_struct(&archive); @@ -15055,15 +17019,14 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) // selected_paths contains paths and its uncompressed size. The size is used to distinguish between files with same path. std::vector> selected_paths; FileArchiveDialog dlg(static_cast(wxGetApp().mainframe), &archive, selected_paths); - if (dlg.ShowModal() == wxID_OK) - { + if (dlg.ShowModal() == wxID_OK) { std::string archive_path_string = archive_path.string(); - archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); + archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data()); for (auto& path_w_size : selected_paths) { const fs::path& path = path_w_size.first; - size_t size = path_w_size.second; + size_t size = path_w_size.second; // find path in zip archive for (mz_uint i = 0; i < num_entries; ++i) { if (mz_zip_reader_file_stat(&archive, i, &stat)) { @@ -15077,29 +17040,29 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) if (path != archive_path) continue; // decompressing - try - { + try { std::replace(name.begin(), name.end(), '\\', '/'); // rename if file exists - std::string filename = path.filename().string(); - std::string extension = path.extension().string(); - std::string just_filename = filename.substr(0, filename.size() - extension.size()); + std::string filename = path.filename().string(); + std::string extension = path.extension().string(); + std::string just_filename = filename.substr(0, filename.size() - extension.size()); std::string final_filename = just_filename; size_t version = 0; - while (fs::exists(archive_dir / (final_filename + extension))) - { + while (fs::exists(archive_dir / (final_filename + extension))) { ++version; final_filename = just_filename + "(" + std::to_string(version) + ")"; } - filename = final_filename + extension; + filename = final_filename + extension; fs::path final_path = archive_dir / filename; - std::string buffer((size_t)stat.m_uncomp_size, 0); + std::string buffer((size_t) stat.m_uncomp_size, 0); // Decompress action. We already has correct file index in stat structure. - mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*) buffer.data(), + (size_t) stat.m_uncomp_size, 0); if (res == 0) { // TRN: First argument = path to file, second argument = error description - wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive))); + wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), + mz_zip_get_error_string(mz_zip_get_last_error(&archive))); BOOST_LOG_TRIVIAL(error) << error_log; show_error(nullptr, error_log); break; @@ -15109,7 +17072,9 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) file.write(buffer.c_str(), buffer.size()); file.close(); if (!fs::exists(final_path)) { - wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string()); + wxString error_log = + GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), + final_path.string()); BOOST_LOG_TRIVIAL(error) << error_log; show_error(nullptr, error_log); break; @@ -15128,9 +17093,7 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) project_paths.emplace_back(final_path); break; - } - catch (const std::exception& e) - { + } catch (const std::exception& e) { // ensure the zip archive is closed and rethrow the exception close_zip_reader(&archive); throw Slic3r::FileIOError(e.what()); @@ -15140,30 +17103,25 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) } close_zip_reader(&archive); if (non_project_paths.size() + project_paths.size() != selected_paths.size()) - BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " - << selected_paths.size() - << " Decopressed files: " - << non_project_paths.size() + project_paths.size(); + BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " << selected_paths.size() + << " Decopressed files: " << non_project_paths.size() + project_paths.size(); } else { close_zip_reader(&archive); return false; } - } - catch (const Slic3r::FileIOError& e) { + } catch (const Slic3r::FileIOError& e) { // zip reader should be already closed or not even opened GUI::show_error(this, e.what()); return false; } // none selected - if (project_paths.empty() && non_project_paths.empty()) - { + if (project_paths.empty() && non_project_paths.empty()) { return false; } // 1 project file and some models - behave like drag n drop of 3mf and then load models - if (project_paths.size() == 1) - { + if (project_paths.size() == 1) { wxArrayString aux; aux.Add(from_u8(project_paths.front().string())); bool loaded3mf = load_files(aux); @@ -15188,7 +17146,6 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) load_files(project_paths, LoadStrategy::LoadModel); load_files(non_project_paths, LoadStrategy::LoadModel); - for (const fs::path& path : project_paths) { // Delete file from temp file (path variable), it will stay only in app memory. boost::system::error_code ec; @@ -15212,32 +17169,32 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) class ProjectDropDialog : public DPIDialog { private: - wxColour m_def_color = wxColour(255, 255, 255); - int m_action{1}; - bool m_remember_choice{false}; + wxColour m_def_color = wxColour(255, 255, 255); + int m_action{1}; + bool m_remember_choice{false}; public: - ProjectDropDialog(const std::string &filename); + ProjectDropDialog(const std::string& filename); - wxPanel * m_top_line; - wxStaticText *m_fname_title; - wxStaticText *m_fname_f; - StaticBox * m_panel_select; + wxPanel* m_top_line; + wxStaticText* m_fname_title; + wxStaticText* m_fname_f; + StaticBox* m_panel_select; - void on_select_ok(wxCommandEvent &event); - void on_select_cancel(wxCommandEvent &event); + void on_select_ok(wxCommandEvent& event); + void on_select_cancel(wxCommandEvent& event); - int get_action() const { return m_action; } - void set_action(int index) { m_action = index; } + int get_action() const { return m_action; } + void set_action(int index) { m_action = index; } - wxBoxSizer *create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip); + wxBoxSizer* create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip); protected: - void on_dpi_changed(const wxRect &suggested_rect) override; + void on_dpi_changed(const wxRect& suggested_rect) override; }; -ProjectDropDialog::ProjectDropDialog(const std::string &filename) - : DPIDialog(static_cast(wxGetApp().mainframe), +ProjectDropDialog::ProjectDropDialog(const std::string& filename) + : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, from_u8((boost::format(_utf8(L("Drop project file")))).str()), wxDefaultPosition, @@ -15248,7 +17205,7 @@ ProjectDropDialog::ProjectDropDialog(const std::string &filename) // def setting SetBackgroundColour(m_def_color); - wxBoxSizer *m_sizer_main = new wxBoxSizer(wxVERTICAL); + wxBoxSizer* m_sizer_main = new wxBoxSizer(wxVERTICAL); m_top_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); m_top_line->SetBackgroundColour(wxColour(166, 169, 170)); @@ -15264,24 +17221,25 @@ ProjectDropDialog::ProjectDropDialog(const std::string &filename) m_fname_f = new wxStaticText(this, wxID_ANY, filename); m_fname_f->SetFont(::Label::Head_14); - m_fname_f->SetMaxSize(wxSize(FromDIP(300),-1)); + m_fname_f->SetMaxSize(wxSize(FromDIP(300), -1)); m_fname_f->Wrap(FromDIP(300)); m_fname_f->SetForegroundColour(wxColour("#363636")); m_sizer_main->Add(m_fname_title, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); m_sizer_main->AddSpacer(FromDIP(10)); - m_sizer_main->Add(m_fname_f , 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); + m_sizer_main->Add(m_fname_f, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); m_sizer_main->AddSpacer(FromDIP(10)); - auto radio_group = new RadioGroup(this, { - _L("Open as project"), // 0 - _L("Import geometry only") // 1 - }, wxVERTICAL); - radio_group->SetMinSize(wxSize(FromDIP(300),-1)); + auto radio_group = new RadioGroup(this, + { + _L("Open as project"), // 0 + _L("Import geometry only") // 1 + }, + wxVERTICAL); + radio_group->SetMinSize(wxSize(FromDIP(300), -1)); radio_group->SetSelection(get_action() - 1); - radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this, radio_group](wxCommandEvent &e) { - set_action(radio_group->GetSelection() + 1); - }); + radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, + [this, radio_group](wxCommandEvent& e) { set_action(radio_group->GetSelection() + 1); }); m_sizer_main->Add(radio_group, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); @@ -15291,8 +17249,8 @@ ProjectDropDialog::ProjectDropDialog(const std::string &filename) // Orca: hide the "Don't show again" checkbox, people keeps accidentally checked this then forgot // wxBoxSizer *m_sizer_left = new wxBoxSizer(wxHORIZONTAL); // - // auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, under 'Load Behaviour'.")); - // m_sizer_left->Add(dont_show_again, 0, wxALL, 5); + // auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, + // under 'Load Behaviour'.")); m_sizer_left->Add(dont_show_again, 0, wxALL, 5); // // m_sizer_bottom->Add(m_sizer_left, 0, wxEXPAND, 5); @@ -15312,9 +17270,9 @@ ProjectDropDialog::ProjectDropDialog(const std::string &filename) wxGetApp().UpdateDlgDarkUI(this); } -wxBoxSizer *ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow *parent, wxString tooltip) +wxBoxSizer* ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip) { - wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); auto checkbox = new ::CheckBox(parent); @@ -15324,13 +17282,13 @@ wxBoxSizer *ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 8); auto checkbox_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxSize(-1, -1), 0); - checkbox_title->SetForegroundColour(wxColour(144,144,144)); + checkbox_title->SetForegroundColour(wxColour(144, 144, 144)); checkbox_title->SetFont(::Label::Body_13); checkbox_title->Wrap(-1); checkbox_title->SetToolTip(tooltip); m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox](wxCommandEvent &e) { + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox](wxCommandEvent& e) { m_remember_choice = checkbox->GetValue(); e.Skip(); }); @@ -15338,28 +17296,24 @@ wxBoxSizer *ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow return m_sizer_checkbox; } -void ProjectDropDialog::on_select_ok(wxCommandEvent &event) +void ProjectDropDialog::on_select_ok(wxCommandEvent& event) { if (m_remember_choice) { LoadType load_type = static_cast(get_action()); - switch (load_type) - { - case LoadType::OpenProject: - wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL); - break; - case LoadType::LoadGeometry: - wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY); - break; + switch (load_type) { + case LoadType::OpenProject: + wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL); + break; + case LoadType::LoadGeometry: + wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY); + break; } } EndModal(wxID_OK); } -void ProjectDropDialog::on_select_cancel(wxCommandEvent &event) -{ - EndModal(wxID_CANCEL); -} +void ProjectDropDialog::on_select_cancel(wxCommandEvent& event) { EndModal(wxID_CANCEL); } void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) { @@ -15367,7 +17321,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) Refresh(); } -//BBS: remove GCodeViewer as seperate APP logic +// BBS: remove GCodeViewer as seperate APP logic bool Plater::load_files(const wxArrayString& filenames) { const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc)", std::regex::icase); @@ -15386,14 +17340,14 @@ bool Plater::load_files(const wxArrayString& filenames) continue; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": normal_paths %1%, gcode_paths %2%")%normal_paths.size() %gcode_paths.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": normal_paths %1%, gcode_paths %2%") % normal_paths.size() % gcode_paths.size(); if (normal_paths.empty() && gcode_paths.empty()) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": can not find valid path, return directly"); // Likely no supported files return false; - } - else if (normal_paths.empty()){ - //only gcode files + } else if (normal_paths.empty()) { + // only gcode files if (gcode_paths.size() > 1) { show_info(this, _L("Only one G-code file can be opened at a time."), _L("G-code loading")); return false; @@ -15408,12 +17362,12 @@ bool Plater::load_files(const wxArrayString& filenames) } //// searches for project files - //for (std::vector::const_reverse_iterator it = normal_paths.rbegin(); it != normal_paths.rend(); ++it) { - // std::string filename = (*it).filename().string(); - // ////BBS: only 3mf will be treated as project file - // if (open_3mf_file((*it))) - // return true; - //} + // for (std::vector::const_reverse_iterator it = normal_paths.rbegin(); it != normal_paths.rend(); ++it) { + // std::string filename = (*it).filename().string(); + // ////BBS: only 3mf will be treated as project file + // if (open_3mf_file((*it))) + // return true; + // } //// other files std::string snapshot_label; @@ -15432,20 +17386,31 @@ bool Plater::load_files(const wxArrayString& filenames) } } - //Plater::TakeSnapshot snapshot(this, snapshot_label); - //load_files(normal_paths, LoadStrategy::LoadModel); + // Plater::TakeSnapshot snapshot(this, snapshot_label); + // load_files(normal_paths, LoadStrategy::LoadModel); // BBS: check file types - std::sort(normal_paths.begin(), normal_paths.end(), [](fs::path obj1, fs::path obj2) { return obj1.filename().string() < obj2.filename().string(); }); + std::sort(normal_paths.begin(), normal_paths.end(), + [](fs::path obj1, fs::path obj2) { return obj1.filename().string() < obj2.filename().string(); }); auto loadfiles_type = LoadFilesType::NoFile; auto amf_files_count = get_3mf_file_count(normal_paths); - if (normal_paths.size() > 1 && amf_files_count < normal_paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } - if (normal_paths.size() > 1 && amf_files_count == normal_paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } - if (normal_paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } - if (normal_paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; - if (normal_paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; + if (normal_paths.size() > 1 && amf_files_count < normal_paths.size()) { + loadfiles_type = LoadFilesType::Multiple3MFOther; + } + if (normal_paths.size() > 1 && amf_files_count == normal_paths.size()) { + loadfiles_type = LoadFilesType::Multiple3MF; + } + if (normal_paths.size() > 1 && amf_files_count == 0) { + loadfiles_type = LoadFilesType::MultipleOther; + } + if (normal_paths.size() == 1 && amf_files_count == 1) { + loadfiles_type = LoadFilesType::Single3MF; + }; + if (normal_paths.size() == 1 && amf_files_count == 0) { + loadfiles_type = LoadFilesType::SingleOther; + }; auto first_file = std::vector{}; auto tmf_file = std::vector{}; @@ -15453,8 +17418,7 @@ bool Plater::load_files(const wxArrayString& filenames) auto res = true; if (this->m_only_gcode || this->m_exported_file) { - if ((loadfiles_type == LoadFilesType::SingleOther) - || (loadfiles_type == LoadFilesType::MultipleOther)) { + if ((loadfiles_type == LoadFilesType::SingleOther) || (loadfiles_type == LoadFilesType::MultipleOther)) { show_info(this, _L("Unable to add models in preview mode"), _L("Add Models")); return false; } @@ -15476,38 +17440,46 @@ bool Plater::load_files(const wxArrayString& filenames) }; switch (loadfiles_type) { - case LoadFilesType::Single3MF: - open_3mf_file(normal_paths[0]); - break; + case LoadFilesType::Single3MF: open_3mf_file(normal_paths[0]); break; case LoadFilesType::SingleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); - if (handle_zips(normal_paths)) return true; - if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { res = false; } + if (handle_zips(normal_paths)) + return true; + if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { + res = false; + } break; } case LoadFilesType::Multiple3MF: first_file = std::vector{normal_paths[0]}; for (auto i = 0; i < normal_paths.size(); i++) { - if (i > 0) { other_file.push_back(normal_paths[i]); } + if (i > 0) { + other_file.push_back(normal_paths[i]); + } }; open_3mf_file(first_file[0]); - if (load_files(other_file, LoadStrategy::LoadModel).empty()) { res = false; } + if (load_files(other_file, LoadStrategy::LoadModel).empty()) { + res = false; + } break; case LoadFilesType::MultipleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); if (handle_zips(normal_paths)) { - if (normal_paths.empty()) return true; + if (normal_paths.empty()) + return true; + } + if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { + res = false; } - if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { res = false; } break; } case LoadFilesType::Multiple3MFOther: - for (const auto &path : normal_paths) { - if (boost::iends_with(path.filename().string(), ".3mf")){ + for (const auto& path : normal_paths) { + if (boost::iends_with(path.filename().string(), ".3mf")) { if (first_file.size() <= 0) first_file.push_back(path); else @@ -15518,11 +17490,16 @@ bool Plater::load_files(const wxArrayString& filenames) } open_3mf_file(first_file[0]); - if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { res = false; } - if (res && handle_zips(other_file)) { - if (normal_paths.empty()) return true; + if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { + res = false; + } + if (res && handle_zips(other_file)) { + if (normal_paths.empty()) + return true; + } + if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { + res = false; } - if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { res = false; } break; default: break; } @@ -15545,7 +17522,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting) } else if (setting == OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK) { ProjectDropDialog dlg(filename); if (dlg.ShowModal() == wxID_OK) { - int choice = dlg.get_action(); + int choice = dlg.get_action(); LoadType load_type = static_cast(choice); wxGetApp().app_config->set("import_project_action", std::to_string(choice)); @@ -15560,38 +17537,42 @@ LoadType determine_load_type(std::string filename, std::string override_setting) } } -bool Plater::open_3mf_file(const fs::path &file_path) +bool Plater::open_3mf_file(const fs::path& file_path) { std::string filename = encode_path(file_path.filename().string().c_str()); if (!boost::algorithm::iends_with(filename, ".3mf")) { return false; } - bool not_empty_plate = !model().objects.empty(); - bool load_setting_ask_when_relevant = wxGetApp().app_config->get(SETTING_PROJECT_LOAD_BEHAVIOUR) == OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT; - LoadType load_type = determine_load_type(filename, (not_empty_plate && load_setting_ask_when_relevant) ? OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK : ""); + bool not_empty_plate = !model().objects.empty(); + bool load_setting_ask_when_relevant = wxGetApp().app_config->get(SETTING_PROJECT_LOAD_BEHAVIOUR) == + OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT; + LoadType load_type = determine_load_type(filename, (not_empty_plate && load_setting_ask_when_relevant) ? + OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK : + ""); - if (load_type == LoadType::Unknown) return false; + if (load_type == LoadType::Unknown) + return false; switch (load_type) { - case LoadType::OpenProject: { - if (wxGetApp().can_load_project()) - load_project(from_path(file_path), ""); - break; - } - case LoadType::LoadGeometry: { - Plater::TakeSnapshot snapshot(this, "Import Object"); - load_files({file_path}, LoadStrategy::LoadModel); - break; - } - case LoadType::LoadConfig: { - load_files({file_path}, LoadStrategy::LoadConfig); - break; - } - case LoadType::Unknown: { - assert(false); - break; - } + case LoadType::OpenProject: { + if (wxGetApp().can_load_project()) + load_project(from_path(file_path), ""); + break; + } + case LoadType::LoadGeometry: { + Plater::TakeSnapshot snapshot(this, "Import Object"); + load_files({file_path}, LoadStrategy::LoadModel); + break; + } + case LoadType::LoadConfig: { + load_files({file_path}, LoadStrategy::LoadConfig); + break; + } + case LoadType::Unknown: { + assert(false); + break; + } } return true; @@ -15600,7 +17581,7 @@ bool Plater::open_3mf_file(const fs::path &file_path) int Plater::get_3mf_file_count(std::vector paths) { auto count = 0; - for (const auto &path : paths) { + for (const auto& path : paths) { if (boost::iends_with(path.filename().string(), ".3mf")) { count++; } @@ -15613,10 +17594,12 @@ void Plater::add_file() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry"; wxArrayString input_files; wxGetApp().import_model(this, input_files); - if (input_files.empty()) return; + if (input_files.empty()) + return; std::vector paths; - for (const auto &file : input_files) paths.emplace_back(into_path(file)); + for (const auto& file : input_files) + paths.emplace_back(into_path(file)); std::string snapshot_label; assert(!paths.empty()); @@ -15633,21 +17616,28 @@ void Plater::add_file() auto loadfiles_type = LoadFilesType::NoFile; auto amf_files_count = get_3mf_file_count(paths); - if (paths.size() > 1 && amf_files_count < paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } - if (paths.size() > 1 && amf_files_count == paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } - if (paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } - if (paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; - if (paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; + if (paths.size() > 1 && amf_files_count < paths.size()) { + loadfiles_type = LoadFilesType::Multiple3MFOther; + } + if (paths.size() > 1 && amf_files_count == paths.size()) { + loadfiles_type = LoadFilesType::Multiple3MF; + } + if (paths.size() > 1 && amf_files_count == 0) { + loadfiles_type = LoadFilesType::MultipleOther; + } + if (paths.size() == 1 && amf_files_count == 1) { + loadfiles_type = LoadFilesType::Single3MF; + }; + if (paths.size() == 1 && amf_files_count == 0) { + loadfiles_type = LoadFilesType::SingleOther; + }; auto first_file = std::vector{}; auto tmf_file = std::vector{}; auto other_file = std::vector{}; - switch (loadfiles_type) - { - case LoadFilesType::Single3MF: - open_3mf_file(paths[0]); - break; + switch (loadfiles_type) { + case LoadFilesType::Single3MF: open_3mf_file(paths[0]); break; case LoadFilesType::SingleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); @@ -15665,11 +17655,15 @@ void Plater::add_file() case LoadFilesType::Multiple3MF: first_file = std::vector{paths[0]}; for (auto i = 0; i < paths.size(); i++) { - if (i > 0) { other_file.push_back(paths[i]); } + if (i > 0) { + other_file.push_back(paths[i]); + } }; open_3mf_file(first_file[0]); - if (!load_files(other_file, LoadStrategy::LoadModel).empty()) { wxGetApp().mainframe->update_title(); } + if (!load_files(other_file, LoadStrategy::LoadModel).empty()) { + wxGetApp().mainframe->update_title(); + } break; case LoadFilesType::MultipleOther: { @@ -15681,13 +17675,13 @@ void Plater::add_file() } wxGetApp().mainframe->update_title(); if (wxGetApp().app_config->get("recent_models") == "true") - for (auto &path : paths) + for (auto& path : paths) wxGetApp().mainframe->add_to_recent_projects(path.wstring()); } break; } case LoadFilesType::Multiple3MFOther: - for (const auto &path : paths) { + for (const auto& path : paths) { if (boost::iends_with(path.filename().string(), ".3mf")) { if (first_file.size() <= 0) first_file.push_back(path); @@ -15703,11 +17697,11 @@ void Plater::add_file() if (!load_files(other_file, LoadStrategy::LoadModel, false).empty()) { wxGetApp().mainframe->update_title(); if (wxGetApp().app_config->get("recent_models") == "true") - for (auto &file : other_file) + for (auto& file : other_file) wxGetApp().mainframe->add_to_recent_projects(file.wstring()); } break; - default:break; + default: break; } } @@ -15716,33 +17710,31 @@ void Plater::update(bool conside_update_flag, bool force_background_processing_u if (is_new_project_and_check_state()) { return; } - unsigned int flag = force_background_processing_update ? (unsigned int)Plater::priv::UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE : 0; + unsigned int flag = force_background_processing_update ? (unsigned int) Plater::priv::UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE : + 0; if (conside_update_flag) { if (need_update()) { p->update(flag); p->set_need_update(false); } - } - else + } else p->update(flag); } void Plater::object_list_changed() { p->object_list_changed(); } -Worker &Plater::get_ui_job_worker() { return p->m_worker; } +Worker& Plater::get_ui_job_worker() { return p->m_worker; } -const Worker &Plater::get_ui_job_worker() const { return p->m_worker; } +const Worker& Plater::get_ui_job_worker() const { return p->m_worker; } void Plater::update_ui_from_settings() { p->update_ui_from_settings(); } void Plater::select_view(const std::string& direction) { p->select_view(direction); } -//BBS: add no_slice logic +// BBS: add no_slice logic void Plater::select_view_3D(const std::string& name, bool no_slice) { p->select_view_3D(name, no_slice); } -void Plater::reload_paint_after_background_process_apply() { - p->preview->set_reload_paint_after_background_process_apply(true); -} +void Plater::reload_paint_after_background_process_apply() { p->preview->set_reload_paint_after_background_process_apply(true); } bool Plater::is_preview_shown() const { return p->is_preview_shown(); } bool Plater::is_preview_loaded() const { return p->is_preview_loaded(); } @@ -15752,7 +17744,7 @@ bool Plater::are_view3D_labels_shown() const { return p->are_view3D_labels_shown void Plater::show_view3D_labels(bool show) { p->show_view3D_labels(show); } bool Plater::is_view3D_overhang_shown() const { return p->is_view3D_overhang_shown(); } -void Plater::show_view3D_overhang(bool show) { p->show_view3D_overhang(show); } +void Plater::show_view3D_overhang(bool show) { p->show_view3D_overhang(show); } bool Plater::is_sidebar_enabled() const { return p->sidebar_layout.is_enabled; } void Plater::enable_sidebar(bool enabled) { p->enable_sidebar(enabled); } @@ -15762,7 +17754,7 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_ void Plater::reset_window_layout() { p->reset_window_layout(); } -//BBS +// BBS void Plater::select_curr_plate_all() { p->select_curr_plate_all(); } void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); } @@ -15774,9 +17766,10 @@ void Plater::remove(size_t obj_idx) { p->remove(obj_idx); } void Plater::reset(bool apply_presets_change) { p->reset(apply_presets_change); } void Plater::reset_with_confirm() { - if (p->model.objects.empty() || MessageDialog(static_cast(this), _L("All objects will be removed, continue?"), - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Delete All"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE) - .ShowModal() == wxID_YES) { + if (p->model.objects.empty() || + MessageDialog(static_cast(this), _L("All objects will be removed, continue?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Delete All"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE) + .ShowModal() == wxID_YES) { reset(); // BBS: jump to plater panel wxGetApp().mainframe->select_tab(TAB_ID_HOME); @@ -15787,13 +17780,15 @@ void Plater::reset_with_confirm() int GUI::Plater::close_with_confirm(std::function second_check) { if (up_to_date(false, false)) { - if (second_check && !second_check(false)) return wxID_CANCEL; + if (second_check && !second_check(false)) + return wxID_CANCEL; model().set_backup_path(""); return wxID_NO; } - MessageDialog dlg(static_cast(this), _L("The current project has unsaved changes. Would you like to save before continuing\?"), - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE); + MessageDialog dlg(static_cast(this), + _L("The current project has unsaved changes. Would you like to save before continuing\?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE); dlg.show_dsa_button(_L("Remember my choice.")); auto choise = wxGetApp().app_config->get("save_project_choise"); auto result = choise.empty() ? dlg.ShowModal() : choise == "yes" ? wxID_YES : wxID_NO; @@ -15813,7 +17808,8 @@ int GUI::Plater::close_with_confirm(std::function second_check) } } - if (second_check && !second_check(result == wxID_YES)) return wxID_CANCEL; + if (second_check && !second_check(result == wxID_YES)) + return wxID_CANCEL; model().set_backup_path(""); up_to_date(true, false); @@ -15822,23 +17818,21 @@ int GUI::Plater::close_with_confirm(std::function second_check) return result; } -//BBS: trigger a restore project event +// BBS: trigger a restore project event void Plater::trigger_restore_project(int skip_confirm) { auto evt = new wxCommandEvent(EVT_RESTORE_PROJECT, this->GetId()); evt->SetInt(skip_confirm); wxQueueEvent(this, evt); - //wxPostEvent(this, *evt); + // wxPostEvent(this, *evt); } -//BBS -bool Plater::delete_object_from_model(size_t obj_idx, bool refresh_immediately) { return p->delete_object_from_model(obj_idx, refresh_immediately); } +// BBS +bool Plater::delete_object_from_model(size_t obj_idx, bool refresh_immediately) +{ return p->delete_object_from_model(obj_idx, refresh_immediately); } -//BBS: delete all from model -void Plater::delete_all_objects_from_model() -{ - p->delete_all_objects_from_model(); -} +// BBS: delete all from model +void Plater::delete_all_objects_from_model() { p->delete_all_objects_from_model(); } void Plater::set_selected_visible(bool visible) { @@ -15851,7 +17845,6 @@ void Plater::set_selected_visible(bool visible) p->get_current_canvas3D()->set_selected_visible(visible); } - void Plater::remove_selected() { /*if (p->get_selection().is_empty()) @@ -15866,30 +17859,33 @@ void Plater::remove_selected() Plater::TakeSnapshot snapshot(this, "Delete Selected Objects"); get_ui_job_worker().cancel_all(); - //BBS delete current selected - // p->view3D->delete_selected(); + // BBS delete current selected + // p->view3D->delete_selected(); p->get_current_canvas3D()->delete_selected(); } void Plater::increase_instances(size_t num) { - if (! can_increase_instances()) { return; } + if (!can_increase_instances()) { + return; + } Plater::TakeSnapshot snapshot(this, "Increase Instances"); int obj_idx = p->get_selected_object_idx(); - ModelObject* model_object = p->model.objects[obj_idx]; + ModelObject* model_object = p->model.objects[obj_idx]; ModelInstance* model_instance = model_object->instances.back(); - bool was_one_instance = model_object->instances.size()==1; + bool was_one_instance = model_object->instances.size() == 1; double offset_base = canvas3D()->get_size_proportional_to_max_bed_size(0.05); - double offset = offset_base; + double offset = offset_base; for (size_t i = 0; i < num; i++, offset += offset_base) { Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0); - model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); -// p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); + model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), + model_instance->get_mirror()); + // p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); } #ifdef SUPPORT_AUTO_CENTER @@ -15899,21 +17895,23 @@ void Plater::increase_instances(size_t num) p->update(); - p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); + p->get_selection().add_instance(obj_idx, (int) model_object->instances.size() - 1); sidebar().obj_list()->increase_object_instances(obj_idx, was_one_instance ? num + 1 : num); p->selection_changed(); this->p->schedule_background_process(); - //if (wxGetApp().app_config->get("auto_arrange") == "true") { - // this->set_prepare_state(Job::PREPARE_STATE_MENU); - // this->arrange(); - //} + // if (wxGetApp().app_config->get("auto_arrange") == "true") { + // this->set_prepare_state(Job::PREPARE_STATE_MENU); + // this->arrange(); + // } } void Plater::decrease_instances(size_t num) { - if (! can_decrease_instances()) { return; } + if (!can_decrease_instances()) { + return; + } Plater::TakeSnapshot snapshot(this, "Decrease Instances"); @@ -15921,34 +17919,28 @@ void Plater::decrease_instances(size_t num) ModelObject* model_object = p->model.objects[obj_idx]; if (model_object->instances.size() > num) { - for (size_t i = 0; i < num; ++ i) + for (size_t i = 0; i < num; ++i) model_object->delete_last_instance(); p->update(); // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar().obj_list()->decrease_object_instances(obj_idx, num); - } - else { + } else { remove(obj_idx); } if (!model_object->instances.empty()) - p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); + p->get_selection().add_instance(obj_idx, (int) model_object->instances.size() - 1); p->selection_changed(); this->p->schedule_background_process(); - //if (wxGetApp().app_config->get("auto_arrange") == "true") { - // this->set_prepare_state(Job::PREPARE_STATE_MENU); - // this->arrange(); - //} + // if (wxGetApp().app_config->get("auto_arrange") == "true") { + // this->set_prepare_state(Job::PREPARE_STATE_MENU); + // this->arrange(); + // } } -static long GetNumberFromUser( const wxString& msg, - const wxString& prompt, - const wxString& title, - long value, - long min, - long max, - wxWindow* parent) +static long GetNumberFromUser( + const wxString& msg, const wxString& prompt, const wxString& title, long value, long min, long max, wxWindow* parent) { #ifdef _WIN32 wxNumberEntryDialog dialog(parent, msg, prompt, title, value, min, max, wxDefaultPosition); @@ -15970,14 +17962,14 @@ void Plater::set_number_of_copies(/*size_t num*/) ModelObject* model_object = p->model.objects[obj_idx]; - const int num = GetNumberFromUser( " ", _L("Number of copies:"), - _L("Copies of the selected object"), model_object->instances.size(), 0, 1000, this ); + const int num = GetNumberFromUser(" ", _L("Number of copies:"), _L("Copies of the selected object"), model_object->instances.size(), 0, + 1000, this); if (num < 0) return; - Plater::TakeSnapshot snapshot(this, (boost::format("Set numbers of copies to %1%")%num).str()); + Plater::TakeSnapshot snapshot(this, (boost::format("Set numbers of copies to %1%") % num).str()); - int diff = num - (int)model_object->instances.size(); + int diff = num - (int) model_object->instances.size(); if (diff > 0) increase_instances(diff); else if (diff < 0) @@ -15986,7 +17978,7 @@ void Plater::set_number_of_copies(/*size_t num*/) void Plater::fill_bed_with_copies() { - auto &w = get_ui_job_worker(); + auto& w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Arrange")); replace_job(w, std::make_unique()); @@ -15995,22 +17987,16 @@ void Plater::fill_bed_with_copies() void Plater::fill_bed_with_instances() { - auto &w = get_ui_job_worker(); + auto& w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Arrange")); replace_job(w, std::make_unique(true)); } } -bool Plater::is_selection_empty() const -{ - return p->get_selection().is_empty() || p->get_selection().is_wipe_tower(); -} +bool Plater::is_selection_empty() const { return p->get_selection().is_empty() || p->get_selection().is_wipe_tower(); } -void Plater::scale_selection_to_fit_print_volume() -{ - p->scale_selection_to_fit_print_volume(); -} +void Plater::scale_selection_to_fit_print_volume() { p->scale_selection_to_fit_print_volume(); } void Plater::convert_unit(ConversionType conv_type) { @@ -16021,13 +18007,14 @@ void Plater::convert_unit(ConversionType conv_type) TakeSnapshot snapshot(this, conv_type == ConversionType::CONV_FROM_INCH ? "Convert from imperial units" : conv_type == ConversionType::CONV_TO_INCH ? "Revert conversion from imperial units" : - conv_type == ConversionType::CONV_FROM_METER ? "Convert from meters" : "Revert conversion from meters"); + conv_type == ConversionType::CONV_FROM_METER ? "Convert from meters" : + "Revert conversion from meters"); wxBusyCursor wait; ModelObjectPtrs objects; std::reverse(obj_idxs.begin(), obj_idxs.end()); for (int obj_idx : obj_idxs) { - ModelObject *object = p->model.objects[obj_idx]; + ModelObject* object = p->model.objects[obj_idx]; object->convert_units(objects, conv_type, volume_idxs); remove(obj_idx); } @@ -16035,13 +18022,12 @@ void Plater::convert_unit(ConversionType conv_type) p->load_model_objects(objects); Selection& selection = p->view3D->get_canvas3d()->get_selection(); - size_t last_obj_idx = p->model.objects.size() - 1; + size_t last_obj_idx = p->model.objects.size() - 1; if (volume_idxs.empty()) { for (size_t i = 0; i < objects.size(); ++i) - selection.add_object((unsigned int)(last_obj_idx - i), i == 0); - } - else { + selection.add_object((unsigned int) (last_obj_idx - i), i == 0); + } else { for (int vol_idx : volume_idxs) selection.add_volume(last_obj_idx, vol_idx, 0, false); } @@ -16063,9 +18049,9 @@ void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& ne wxGetApp().obj_list()->update_info_items(idx); Selection& selection = p->get_selection(); - size_t last_id = p->model.objects.size() - 1; + size_t last_id = p->model.objects.size() - 1; for (size_t i = 0; i < new_objects.size(); ++i) - selection.add_object((unsigned int)(last_id - i), i == 0); + selection.add_object((unsigned int) (last_id - i), i == 0); // UIThreadWorker w; // arrange(w, true); @@ -16077,8 +18063,8 @@ void Plater::export_gcode(bool prefer_removable) if (p->model.objects.empty()) return; - //if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) - // return; + // if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) + // return; if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) return; @@ -16092,21 +18078,20 @@ void Plater::export_gcode(bool prefer_removable) unsigned int state = this->p->update_restart_background_process(false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) return; - default_output_file = this->p->background_process.output_filepath_for_project( - into_path(this->p->get_project_filename(".3mf"))); - } catch (const Slic3r::PlaceholderParserError &ex) { + default_output_file = this->p->background_process.output_filepath_for_project(into_path(this->p->get_project_filename(".3mf"))); + } catch (const Slic3r::PlaceholderParserError& ex) { // Show the error with monospaced font. show_error(this, ex.what(), true); return; - } catch (const std::exception &ex) { + } catch (const std::exception& ex) { show_error(this, ex.what(), false); return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - AppConfig &appconfig = *wxGetApp().app_config; - RemovableDriveManager &removable_drive_manager = *wxGetApp().removable_drive_manager(); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + AppConfig& appconfig = *wxGetApp().app_config; + RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager(); // Get a last save path, either to removable media or to an internal media. - std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable); + std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable); if (prefer_removable) { // Returns a path to a removable media if it exists, prefering start_dir. Update the internal removable drives database. start_dir = removable_drive_manager.get_removable_drive_path(start_dir); @@ -16118,17 +18103,14 @@ void Plater::export_gcode(bool prefer_removable) fs::path output_path; { std::string ext = default_output_file.extension().string(); - wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SLA file as:"), - start_dir, - from_path(default_output_file.filename()), - GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_SL1, ext), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT - ); + wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SLA file as:"), start_dir, + from_path(default_output_file.filename()), + GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_SL1, ext), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (dlg.ShowModal() == wxID_OK) { output_path = into_path(dlg.GetPath()); while (has_illegal_filename_characters(output_path.filename().string())) { show_error(this, _L("The provided file name is not valid.") + "\n" + - _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); + _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); dlg.SetFilename(from_path(output_path.filename())); if (dlg.ShowModal() == wxID_OK) output_path = into_path(dlg.GetPath()); @@ -16140,17 +18122,17 @@ void Plater::export_gcode(bool prefer_removable) } } - if (! output_path.empty()) { + if (!output_path.empty()) { bool path_on_removable_media = removable_drive_manager.set_and_verify_last_save_path(output_path.string()); - //bool path_on_removable_media = false; + // bool path_on_removable_media = false; p->notification_manager->new_export_began(path_on_removable_media); - p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; - p->last_output_path = output_path.string(); + p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; + p->last_output_path = output_path.string(); p->last_output_dir_path = output_path.parent_path().string(); p->export_gcode(output_path, path_on_removable_media); // Storing a path to AppConfig either as path to removable media or a path to internal media. - // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled the external drives - // while the dialog was open. + // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled + // the external drives while the dialog was open. appconfig.update_last_output_dir(output_path.parent_path().string(), path_on_removable_media); try { @@ -16162,22 +18144,18 @@ void Plater::export_gcode(bool prefer_removable) j["printer_preset"] = printer_config.config.opt_string("inherits"); } - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { j["gcode_printer_model"] = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); } - NetworkAgent *agent = wxGetApp().getAgent(); + NetworkAgent* agent = wxGetApp().getAgent(); } catch (...) {} - } } -void Plater::send_to_printer(bool isall) -{ - p->on_action_send_to_printer(isall); -} +void Plater::send_to_printer(bool isall) { p->on_action_send_to_printer(isall); } -//BBS export gcode 3mf to file +// BBS export gcode 3mf to file void Plater::export_gcode_3mf(bool export_all) { if (p->model.objects.empty()) @@ -16186,7 +18164,7 @@ void Plater::export_gcode_3mf(bool export_all) if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) return; - //calc default_output_file, get default output file from background process + // calc default_output_file, get default output file from background process fs::path default_output_file; AppConfig& appconfig = *wxGetApp().app_config; std::string start_dir; @@ -16196,60 +18174,52 @@ void Plater::export_gcode_3mf(bool export_all) unsigned int state = this->p->update_restart_background_process(false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) return; - default_output_file = this->p->background_process.output_filepath_for_project( - into_path(this->p->get_project_filename(".3mf"))); - } - catch (const Slic3r::PlaceholderParserError& ex) { + default_output_file = this->p->background_process.output_filepath_for_project(into_path(this->p->get_project_filename(".3mf"))); + } catch (const Slic3r::PlaceholderParserError& ex) { // Show the error with monospaced font. show_error(this, ex.what(), true); return; - } - catch (const std::exception& ex) { + } catch (const std::exception& ex) { show_error(this, ex.what(), false); return; } default_output_file.replace_extension(".gcode.3mf"); default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - //Get a last save path + // Get a last save path start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), false); fs::path output_path; { std::string ext = default_output_file.extension().string(); - wxFileDialog dlg(this, _L("Save Sliced file as:"), - start_dir, - from_path(default_output_file.filename()), - GUI::file_wildcards(FT_GCODE_3MF, ""), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT - ); + wxFileDialog dlg(this, _L("Save Sliced file as:"), start_dir, from_path(default_output_file.filename()), + GUI::file_wildcards(FT_GCODE_3MF, ""), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (dlg.ShowModal() == wxID_OK) { output_path = into_path(dlg.GetPath()); - ext = output_path.extension().string(); + ext = output_path.extension().string(); if (ext != ".3mf") output_path = output_path.string() + ".3mf"; } } if (!output_path.empty()) { - //BBS do not set to removable media path + // BBS do not set to removable media path bool path_on_removable_media = false; p->notification_manager->new_export_began(path_on_removable_media); p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; - //BBS do not save last output path - p->last_output_path = output_path.string(); + // BBS do not save last output path + p->last_output_path = output_path.string(); p->last_output_dir_path = output_path.parent_path().string(); - int plate_idx = get_partplate_list().get_curr_plate_index(); + int plate_idx = get_partplate_list().get_curr_plate_index(); if (export_all) plate_idx = PLATE_ALL_IDX; - export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence + export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, + plate_idx); // BBS: silence RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager(); - bool on_removable = removable_drive_manager.is_path_on_removable_drive(p->last_output_dir_path); - // update last output dir appconfig.update_last_output_dir(output_path.parent_path().string(), false); p->notification_manager->push_exporting_finished_notification(output_path.string(), p->last_output_dir_path, on_removable); @@ -16265,7 +18235,9 @@ void Plater::send_gcode_finish(wxString name) void Plater::export_core_3mf() { wxString path = p->get_export_file(FT_3MF); - if (path.empty()) { return; } + if (path.empty()) { + return; + } const std::string path_u8 = into_u8(path); export_3mf(path_u8, SaveStrategy::Silence); } @@ -16273,7 +18245,8 @@ void Plater::export_core_3mf() // Export the current project as a "published" 3MF: a pure export that never touches the // project's file name, dirty state, backup path or title, and attaches the published metadata // to the model only for the duration of the export (a later Save Project is a normal 3MF). -int Plater::export_published_3mf(const std::vector& published_keys, const std::vector& material_keys) +int Plater::export_published_3mf(const std::vector& published_keys, + const std::vector& material_keys) { wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:")); if (path.empty() || path == "") @@ -16284,36 +18257,52 @@ int Plater::export_published_3mf(const std::vector& published_keys, j.push_back(key); nlohmann::json jm = nlohmann::json::array(); for (const Slic3r::PublishedMaterialEntry& e : material_keys) - jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}, {"setting_id", e.setting_id}, {"name", e.preset_name}}}, {"slot", e.slot}, {"keys", e.keys}, - {"full", e.full}, {"full_keys", e.full_keys}, - {"publish_type", e.publish_type}, {"type", e.publish_type_value}, - {"publish_color", e.publish_color}, {"color", e.color} }); + jm.push_back({{"material", + {{"filament_type", e.filament_type}, + {"filament_vendor", e.filament_vendor}, + {"filament_id", e.filament_id}, + {"setting_id", e.setting_id}, + {"name", e.preset_name}}}, + {"slot", e.slot}, + {"keys", e.keys}, + {"full", e.full}, + {"full_keys", e.full_keys}, + {"publish_type", e.publish_type}, + {"type", e.publish_type_value}, + {"publish_color", e.publish_color}, + {"color", e.color}}); Model& model = this->model(); // Save the previous metadata so it can be restored after the export, keeping the in-memory // project pristine (the published flag lives only in the exported file). - const bool had_model_info = (model.model_info != nullptr); - const bool had_published = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG) != model.model_info->metadata_items.end()); - const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG) != model.model_info->metadata_items.end()); - const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG) != model.model_info->metadata_items.end()); - const bool had_payload = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG) != model.model_info->metadata_items.end()); + const bool had_model_info = (model.model_info != nullptr); + const bool had_published = had_model_info && + (model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG) != model.model_info->metadata_items.end()); + const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG) != + model.model_info->metadata_items.end()); + const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG) != + model.model_info->metadata_items.end()); + const bool had_payload = had_model_info && + (model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG) != model.model_info->metadata_items.end()); const std::string prev_published = had_published ? model.model_info->metadata_items.at(ORCA_PUBLISHED_TAG) : std::string(); - const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_KEYS_TAG) : std::string(); - const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_MATERIAL_TAG) : std::string(); + const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_KEYS_TAG) : + std::string(); + const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_MATERIAL_TAG) : + std::string(); const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string(); // export_3mf() assigns archive paths to previously unsaved SVGs. Preserve those fields too, // otherwise a publish changes what a later normal project save writes. - std::vector> previous_svg_paths; - for (ModelObject *object : model.objects) - for (ModelVolume *volume : object->volumes) + std::vector> previous_svg_paths; + for (ModelObject* object : model.objects) + for (ModelVolume* volume : object->volumes) if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) { - std::string *path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf; + std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf; previous_svg_paths.emplace_back(path_in_3mf, *path_in_3mf); } auto restore_temporary_state = [&]() { - for (const auto &[path_in_3mf, previous_path] : previous_svg_paths) + for (const auto& [path_in_3mf, previous_path] : previous_svg_paths) *path_in_3mf = previous_path; if (!had_model_info) { @@ -16338,7 +18327,7 @@ int Plater::export_published_3mf(const std::vector& published_keys, } }; bool state_restored = false; - auto restore_now = [&]() { + auto restore_now = [&]() { if (state_restored) return; restore_temporary_state(); @@ -16348,8 +18337,8 @@ int Plater::export_published_3mf(const std::vector& published_keys, if (model.model_info == nullptr) model.model_info = std::make_shared(); - model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; - model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump(); + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump(); model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = jm.dump(); int ret = -1; @@ -16360,57 +18349,65 @@ int Plater::export_published_3mf(const std::vector& published_keys, // filtered values are serialized into the published_config metadata payload instead, so // OrcaSlicer versions without the publish feature fall back to importing the geometry only // (keeping the receiver's presets) while new versions rebuild the config from the payload. - DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); std::string payload; - for (const std::string &key : filtered_cfg.keys()) + for (const std::string& key : filtered_cfg.keys()) payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload); // Same file layout as save_project(), plus Silence (so export_3mf does not set the project // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished. - auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); if (full_pathnames) save_strategy = save_strategy | SaveStrategy::FullPathSources; ret = export_3mf(into_path(path), save_strategy, -1, nullptr); } catch (...) { restore_now(); - MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), - _L("Publish"), wxOK | wxICON_WARNING).ShowModal(); + MessageDialog(this, + _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs " + "have the file open."), + _L("Publish"), wxOK | wxICON_WARNING) + .ShowModal(); return wxID_CANCEL; } if (ret < 0) { restore_now(); - MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), - _L("Publish"), wxOK | wxICON_WARNING).ShowModal(); + MessageDialog(this, + _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs " + "have the file open."), + _L("Publish"), wxOK | wxICON_WARNING) + .ShowModal(); return wxID_CANCEL; } restore_now(); return wxID_YES; } -Preset *get_printer_preset(const MachineObject *obj) +Preset* get_printer_preset(const MachineObject* obj) { if (!obj) return nullptr; - Preset *printer_preset = nullptr; + Preset* printer_preset = nullptr; - PresetBundle *preset_bundle = wxGetApp().preset_bundle; + PresetBundle* preset_bundle = wxGetApp().preset_bundle; for (auto printer_it = preset_bundle->printers.begin(); printer_it != preset_bundle->printers.end(); printer_it++) { // only use system printer preset if (!printer_it->is_system) continue; - ConfigOption *printer_nozzle_opt = printer_it->config.option("nozzle_diameter"); - ConfigOptionFloats *printer_nozzle_vals = nullptr; - if (printer_nozzle_opt) printer_nozzle_vals = dynamic_cast(printer_nozzle_opt); + ConfigOption* printer_nozzle_opt = printer_it->config.option("nozzle_diameter"); + ConfigOptionFloats* printer_nozzle_vals = nullptr; + if (printer_nozzle_opt) + printer_nozzle_vals = dynamic_cast(printer_nozzle_opt); std::string model_id = printer_it->get_current_printer_type(preset_bundle); - std::string printer_type = obj->get_show_printer_type(); - bool nozzle_diameter_matches_or_unknown = printer_nozzle_vals && obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, printer_nozzle_vals->get_at(0)); + std::string printer_type = obj->get_show_printer_type(); + bool nozzle_diameter_matches_or_unknown = printer_nozzle_vals && + obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, printer_nozzle_vals->get_at(0)); if (model_id.compare(printer_type) == 0 && nozzle_diameter_matches_or_unknown) { printer_preset = &(*printer_it); } @@ -16418,7 +18415,7 @@ Preset *get_printer_preset(const MachineObject *obj) return printer_preset; } -bool Plater::check_printer_initialized(MachineObject *obj, bool only_warning, bool popup_warning) +bool Plater::check_printer_initialized(MachineObject* obj, bool only_warning, bool popup_warning) { if (!obj) return false; @@ -16427,7 +18424,6 @@ bool Plater::check_printer_initialized(MachineObject *obj, bool only_warning, bo const auto& extruders = obj->GetExtderSystem()->GetExtruders(); for (const DevExtder& extruder : extruders) { - // Skip check if nozzle type is unknown if (extruder.GetNozzleType() == NozzleType::ntUndefine) { continue; @@ -16443,14 +18439,16 @@ bool Plater::check_printer_initialized(MachineObject *obj, bool only_warning, bo if (popup_warning) { if (!only_warning) { if (DevPrinterConfigUtil::get_printer_can_set_nozzle(obj->get_show_printer_type())) { - MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please set the nozzle and try again."), _L("Warning"), wxOK | wxICON_WARNING); + MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please set the nozzle and try again."), + _L("Warning"), wxOK | wxICON_WARNING); dlg.ShowModal(); } else { - MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please check."), _L("Warning"), wxOK | wxICON_WARNING); + MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please check."), _L("Warning"), + wxOK | wxICON_WARNING); dlg.ShowModal(); } - PrinterPartsDialog *print_parts_dlg = new PrinterPartsDialog(nullptr); + PrinterPartsDialog* print_parts_dlg = new PrinterPartsDialog(nullptr); print_parts_dlg->update_machine_obj(obj); print_parts_dlg->ShowModal(); } else { @@ -16471,26 +18469,25 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st std::vector csgmesh; csgmesh.reserve(2 * mo.volumes.size()); bool has_splitable_volume = csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh), - csg::mpartsPositive | csg::mpartsNegative); - + csg::mpartsPositive | csg::mpartsNegative); + std::string fail_msg = _u8L("Unable to perform boolean operation on model meshes. " - "Only positive parts will be kept. You may fix the meshes and try again."); - if (auto fail_reason_name = csg::check_csgmesh_booleans(Range{ std::begin(csgmesh), std::end(csgmesh) }); std::get<0>(fail_reason_name) != csg::BooleanFailReason::OK) { + "Only positive parts will be kept. You may fix the meshes and try again."); + if (auto fail_reason_name = csg::check_csgmesh_booleans(Range{std::begin(csgmesh), std::end(csgmesh)}); + std::get<0>(fail_reason_name) != csg::BooleanFailReason::OK) { std::string name = std::get<1>(fail_reason_name); - std::map fail_reasons = { - {csg::BooleanFailReason::OK, "OK"}, - {csg::BooleanFailReason::MeshEmpty, Slic3r::format( _u8L("Reason: part \"%1%\" is empty."), name)}, - {csg::BooleanFailReason::NotBoundAVolume, Slic3r::format(_u8L("Reason: part \"%1%\" does not bound a volume."), name)}, - {csg::BooleanFailReason::SelfIntersect, Slic3r::format(_u8L("Reason: part \"%1%\" has self intersection."), name)}, - {csg::BooleanFailReason::NoIntersection, Slic3r::format(_u8L("Reason: \"%1%\" and another part have no intersection."), name)} }; + std::map fail_reasons = + {{csg::BooleanFailReason::OK, "OK"}, + {csg::BooleanFailReason::MeshEmpty, Slic3r::format(_u8L("Reason: part \"%1%\" is empty."), name)}, + {csg::BooleanFailReason::NotBoundAVolume, Slic3r::format(_u8L("Reason: part \"%1%\" does not bound a volume."), name)}, + {csg::BooleanFailReason::SelfIntersect, Slic3r::format(_u8L("Reason: part \"%1%\" has self intersection."), name)}, + {csg::BooleanFailReason::NoIntersection, Slic3r::format(_u8L("Reason: \"%1%\" and another part have no intersection."), name)}}; fail_msg += " " + fail_reasons[std::get<0>(fail_reason_name)]; - } - else { + } else { try { - MeshBoolean::mcut::McutMeshPtr meshPtr = csg::perform_csgmesh_booleans_mcut(Range{ std::begin(csgmesh), std::end(csgmesh) }); - mesh = MeshBoolean::mcut::mcut_to_triangle_mesh(*meshPtr); - } - catch (...) {} + MeshBoolean::mcut::McutMeshPtr meshPtr = csg::perform_csgmesh_booleans_mcut(Range{std::begin(csgmesh), std::end(csgmesh)}); + mesh = MeshBoolean::mcut::mcut_to_triangle_mesh(*meshPtr); + } catch (...) {} #if 0 // if mcut fails, try again with CGAL if (mesh.empty()) { @@ -16523,8 +18520,7 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st m.transform(i->get_matrix(), true); mesh.merge(m); } - } - else if (0 <= instance_id && instance_id < int(mo.instances.size())) + } else if (0 <= instance_id && instance_id < int(mo.instances.size())) mesh.transform(mo.instances[instance_id]->get_matrix(), true); return mesh; } @@ -16533,7 +18529,9 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st #define EXPORT_WITH_BOOLEAN 0 void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, FileType file_type) { - if (p->model.objects.empty()) { return; } + if (p->model.objects.empty()) { + return; + } int quality = 0; @@ -16557,31 +18555,35 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } else { path = p->get_export_file(file_type); } - if (path.empty()) { return; } + if (path.empty()) { + return; + } const std::string path_u8 = into_u8(path); wxBusyCursor wait; const auto& selection = p->get_selection(); - const auto obj_idx = selection.get_object_idx(); + const auto obj_idx = selection.get_object_idx(); #if EXPORT_WITH_BOOLEAN if (selection_only && (obj_idx == -1 || selection.is_wipe_tower())) return; #else // BBS support selecting multiple objects - if (selection_only && selection.is_wipe_tower()) return; + if (selection_only && selection.is_wipe_tower()) + return; // BBS if (selection_only) { // only support selection single full object and mulitiple full object - if (!selection.is_single_full_object() && !selection.is_multiple_full_object()) return; + if (!selection.is_single_full_object() && !selection.is_multiple_full_object()) + return; } // Following lambda generates a combined mesh for export with normals pointing outwards. - auto mesh_to_export_fff_no_boolean = [this](const ModelObject &mo, int instance_id) { + auto mesh_to_export_fff_no_boolean = [this](const ModelObject& mo, int instance_id) { TriangleMesh mesh; - //Prusa export negative parts + // Prusa export negative parts std::vector csgmesh; csgmesh.reserve(2 * mo.volumes.size()); csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh), @@ -16593,14 +18595,13 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } else if (std::get<2>(csg::check_csgmesh_booleans(csgrange)) == csgrange.end()) { try { auto cgalm = csg::perform_csgmesh_booleans(csgrange); - mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm); + mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm); } catch (...) {} } if (mesh.empty()) { - get_notification_manager()->push_plater_error_notification( - _u8L("Unable to perform boolean operation on model meshes. " - "Only positive parts will be exported.")); + get_notification_manager()->push_plater_error_notification(_u8L("Unable to perform boolean operation on model meshes. " + "Only positive parts will be exported.")); for (const ModelVolume* v : mo.volumes) if (v->is_model_part()) { @@ -16612,7 +18613,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil if (instance_id == -1) { TriangleMesh vols_mesh(mesh); mesh = TriangleMesh(); - for (const ModelInstance *i : mo.instances) { + for (const ModelInstance* i : mo.instances) { TriangleMesh m = vols_mesh; m.transform(i->get_matrix(), true); mesh.merge(m); @@ -16625,15 +18626,17 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) { TriangleMesh mesh; - const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); + const SLAPrintObject* object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); if (auto m = object->get_mesh_to_print(); m.empty()) - mesh = combine_mesh_fff(mo, instance_id, [this](const std::string& msg) {return get_notification_manager()->push_general_error_notification(msg); }); + mesh = combine_mesh_fff(mo, instance_id, [this](const std::string& msg) { + return get_notification_manager()->push_general_error_notification(msg); + }); else { const Transform3d mesh_trafo_inv = object->trafo().inverse(); - const bool is_left_handed = object->is_left_handed(); + const bool is_left_handed = object->is_left_handed(); - auto pad_mesh = extended? object->pad_mesh() : TriangleMesh{}; + auto pad_mesh = extended ? object->pad_mesh() : TriangleMesh{}; pad_mesh.transform(mesh_trafo_inv); auto supports_mesh = extended ? object->support_mesh() : TriangleMesh{}; @@ -16642,16 +18645,16 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil const std::vector& obj_instances = object->instances(); for (const SLAPrintObject::Instance& obj_instance : obj_instances) { auto it = std::find_if(object->model_object()->instances.begin(), object->model_object()->instances.end(), - [&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; }); + [&obj_instance](const ModelInstance* mi) { return mi->id() == obj_instance.instance_id; }); assert(it != object->model_object()->instances.end()); if (it != object->model_object()->instances.end()) { - const bool one_inst_only = selection_only && ! selection.is_single_full_object(); + const bool one_inst_only = selection_only && !selection.is_single_full_object(); const int instance_idx = it - object->model_object()->instances.begin(); - const Transform3d& inst_transform = one_inst_only - ? Transform3d::Identity() - : object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); + const Transform3d& inst_transform = + one_inst_only ? Transform3d::Identity() : + object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); TriangleMesh inst_mesh; @@ -16674,10 +18677,10 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil inst_mesh.merge(inst_object_mesh); - // ensure that the instance lays on the bed + // ensure that the instance lays on the bed inst_mesh.translate(0.0f, 0.0f, -inst_mesh.bounding_box().min.z()); - // merge instance with global mesh + // merge instance with global mesh mesh.merge(inst_mesh); if (one_inst_only) @@ -16689,20 +18692,22 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil return mesh; }; - std::function - mesh_to_export; + std::function mesh_to_export; if (p->printer_technology == ptFFF) #if EXPORT_WITH_BOOLEAN - mesh_to_export = [this](const ModelObject& mo, int instance_id) {return Plater::combine_mesh_fff(mo, instance_id, - [this](const std::string& msg) {return get_notification_manager()->push_general_error_notification(msg); }); }; + mesh_to_export = [this](const ModelObject& mo, int instance_id) { + return Plater::combine_mesh_fff(mo, instance_id, [this](const std::string& msg) { + return get_notification_manager()->push_general_error_notification(msg); + }); + }; #else mesh_to_export = mesh_to_export_fff_no_boolean; #endif else mesh_to_export = mesh_to_export_sla; - auto get_save_file = [file_type](std::string const & dir, std::string const & name) { + auto get_save_file = [file_type](std::string const& dir, std::string const& name) { std::string ext = ""; switch (file_type) { case FT_STL: ext = ".stl"; break; @@ -16710,39 +18715,38 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } auto path = dir + name + ext; - int n = 1; + int n = 1; while (boost::filesystem::exists(path)) - path = dir + name + "(" + std::to_string(n++) + ")"+ext; + path = dir + name + "(" + std::to_string(n++) + ")" + ext; return path; }; TriangleMesh mesh; if (selection_only) { if (selection.is_single_full_object()) { - const auto obj_idx = selection.get_object_idx(); + const auto obj_idx = selection.get_object_idx(); const ModelObject* model_object = p->model.objects[obj_idx]; if (selection.get_mode() == Selection::Instance) mesh = mesh_to_export(*model_object, (model_object->instances.size() > 1) ? -1 : selection.get_instance_idx()); else { const GLVolume* volume = selection.get_first_volume(); - mesh = model_object->volumes[volume->volume_idx()]->mesh(); + mesh = model_object->volumes[volume->volume_idx()]->mesh(); mesh.transform(volume->get_volume_transformation().get_matrix(), true); } - if (model_object->instances.size() == 1) mesh.translate(-model_object->origin_translation.cast()); - } - else if (selection.is_multiple_full_object() && !multi_stls) { + if (model_object->instances.size() == 1) + mesh.translate(-model_object->origin_translation.cast()); + } else if (selection.is_multiple_full_object() && !multi_stls) { const std::set>& instances_idxs = p->get_selection().get_selected_object_instances(); for (const std::pair& i : instances_idxs) { ModelObject* object = p->model.objects[i.first]; mesh.merge(mesh_to_export(*object, i.second)); } - } - else if (selection.is_multiple_full_object() && multi_stls) { - const std::set> &instances_idxs = p->get_selection().get_selected_object_instances(); - for (const std::pair &i : instances_idxs) { - ModelObject *object = p->model.objects[i.first]; - auto mesh = mesh_to_export(*object, i.second); + } else if (selection.is_multiple_full_object() && multi_stls) { + const std::set>& instances_idxs = p->get_selection().get_selected_object_instances(); + for (const std::pair& i : instances_idxs) { + ModelObject* object = p->model.objects[i.first]; + auto mesh = mesh_to_export(*object, i.second); mesh.translate(-object->origin_translation.cast()); switch (file_type) { @@ -16752,8 +18756,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } return; } - } - else if (!multi_stls) { + } else if (!multi_stls) { for (const ModelObject* o : p->model.objects) { mesh.merge(mesh_to_export(*o, -1)); } @@ -16776,7 +18779,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } } -//BBS: remove amf export +// BBS: remove amf export /*void Plater::export_amf() { if (p->model.objects.empty()) { return; } @@ -16797,7 +18800,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil }*/ namespace { -std::string get_file_name(const std::string &file_path) +std::string get_file_name(const std::string& file_path) { size_t pos_last_delimiter = file_path.find_last_of("/\\"); size_t pos_point = file_path.find_last_of('.'); @@ -16805,33 +18808,33 @@ std::string get_file_name(const std::string &file_path) size_t count = pos_point - pos_last_delimiter - 1; return file_path.substr(offset, count); } -using SvgFile = EmbossShape::SvgFile; +using SvgFile = EmbossShape::SvgFile; using SvgFiles = std::vector; -std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs) +std::string create_unique_3mf_filepath(const std::string& file, const SvgFiles svgs) { // const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp std::string path_in_3mf = "3D/" + file + ".svg"; - size_t suffix_number = 0; - bool is_unique = false; - do{ - is_unique = true; - path_in_3mf = "3D/" + file + ((suffix_number++)? ("_" + std::to_string(suffix_number)) : "") + ".svg"; - for (SvgFile *svgfile : svgs) { + size_t suffix_number = 0; + bool is_unique = false; + do { + is_unique = true; + path_in_3mf = "3D/" + file + ((suffix_number++) ? ("_" + std::to_string(suffix_number)) : "") + ".svg"; + for (SvgFile* svgfile : svgs) { if (svgfile->path_in_3mf.empty()) continue; if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) { is_unique = false; break; } - } + } } while (!is_unique); return path_in_3mf; } -bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs) +bool set_by_local_path(SvgFile& svg, const SvgFiles& svgs) { // Try to find already used svg file - for (SvgFile *svg_ : svgs) { + for (SvgFile* svg_ : svgs) { if (svg_->path_in_3mf.empty()) continue; if (svg.path.compare(svg_->path) == 0) { @@ -16846,13 +18849,13 @@ bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs) /// Function to secure private data before store to 3mf /// /// Data(also private) to clean before publishing -void publish(Model &model, SaveStrategy strategy) { - +void publish(Model& model, SaveStrategy strategy) +{ // SVG file publishing bool exist_new = false; SvgFiles svgfiles; - for (ModelObject *object: model.objects){ - for (ModelVolume *volume : object->volumes) { + for (ModelObject* object : model.objects) { + for (ModelVolume* volume : object->volumes) { if (!volume->emboss_shape.has_value()) continue; if (volume->text_configuration.has_value()) @@ -16869,7 +18872,7 @@ void publish(Model &model, SaveStrategy strategy) { } } - for (SvgFile *svgfile : svgfiles) { + for (SvgFile* svgfile : svgfiles) { if (!svgfile->path_in_3mf.empty()) continue; // already suggested path (previous save) @@ -16881,26 +18884,26 @@ void publish(Model &model, SaveStrategy strategy) { // check whether original filename is already in: filename = get_file_name(svgfile->path); } - svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); + svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); } } -} +} // namespace // BBS: backup int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) { int ret = 0; - //if (p->model.objects.empty()) { - // MessageDialog dialog(nullptr, _L("No objects to export."), _L("Save project"), wxYES); - // if (dialog.ShowModal() == wxYES) - // return -1; - //} + // if (p->model.objects.empty()) { + // MessageDialog dialog(nullptr, _L("No objects to export."), _L("Save project"), wxYES); + // if (dialog.ShowModal() == wxYES) + // return -1; + // } if (output_path.empty()) return -1; bool export_config = true; - wxString path = from_path(output_path); + wxString path = from_path(output_path); if (!path.Lower().EndsWith(".3mf")) return -1; @@ -16909,17 +18912,19 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy // modify model publish(p->model, strategy); - DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; - BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") - %output_path.string()%(strategy & SaveStrategy::Backup)%export_plate_idx %(unsigned int)strategy; + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ + << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") % output_path.string() % + (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int) strategy; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") - % std::string("") % (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int)strategy; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") % std::string("") % + (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int) strategy; - //BBS: add plate logic for thumbnail generate + // BBS: add plate logic for thumbnail generate std::vector thumbnails; std::vector no_light_thumbnails; std::vector calibration_thumbnails; @@ -16930,106 +18935,113 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy if (!(strategy & SaveStrategy::Backup)) { for (int i = 0; i < p->partplate_list.get_plate_count(); i++) { ThumbnailData* thumbnail_data = &p->partplate_list.get_plate(i)->thumbnail_data; - if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { - //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; - } - else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; - const ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i }; - p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, - thumbnail_params, Camera::EType::Ortho); - } - thumbnails.push_back(thumbnail_data); - - ThumbnailData *no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; - if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) { + if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; - p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, - Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); + p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, + thumbnail_params, Camera::EType::Ortho); + } + thumbnails.push_back(thumbnail_data); + + ThumbnailData* no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; + if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) { + // no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; + p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, + THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, + true); } no_light_thumbnails.push_back(no_light_thumbnail_data); - //ThumbnailData* calibration_data = &p->partplate_list.get_plate(i)->cali_thumbnail_data; - //calibration_thumbnails.push_back(calibration_data); + // ThumbnailData* calibration_data = &p->partplate_list.get_plate(i)->cali_thumbnail_data; + // calibration_thumbnails.push_back(calibration_data); PlateBBoxData* plate_bbox_data = &p->partplate_list.get_plate(i)->cali_bboxes_data; plate_bboxes.push_back(plate_bbox_data); - //generate top and picking thumbnails + // generate top and picking thumbnails ThumbnailData* top_thumbnail = &p->partplate_list.get_plate(i)->top_thumbnail_data; - if (top_thumbnail->is_valid() && using_exported_file()) { - //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; - } - else { + if (top_thumbnail->is_valid() && using_exported_file()) { + // no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%") % + i; + } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; - const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; - p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, - Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, false); + const ThumbnailsParams thumbnail_params = {{}, false, true, false, true, i}; + p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, + THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, + false); } top_thumbnails.push_back(top_thumbnail); ThumbnailData* picking_thumbnail = &p->partplate_list.get_plate(i)->pick_thumbnail_data; - if (picking_thumbnail->is_valid() && using_exported_file()) { - //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; - } - else { + if (picking_thumbnail->is_valid() && using_exported_file()) { + // no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%") % + i; + } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; - const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; - p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, - Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, true,true); + const ThumbnailsParams thumbnail_params = {{}, false, true, false, true, i}; + p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, + THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, + true, true); } picking_thumbnails.push_back(picking_thumbnail); } if (p->partplate_list.get_curr_plate()->is_slice_result_valid()) { - //BBS generate BBS calibration thumbnails + // BBS generate BBS calibration thumbnails int index = p->partplate_list.get_curr_plate_index(); - //ThumbnailData* calibration_data = calibration_thumbnails[index]; - //const ThumbnailsParams calibration_params = { {}, false, true, true, true, p->partplate_list.get_curr_plate_index() }; - //p->generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, calibration_params); + // ThumbnailData* calibration_data = calibration_thumbnails[index]; + // const ThumbnailsParams calibration_params = { {}, false, true, true, true, p->partplate_list.get_curr_plate_index() }; + // p->generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, + // calibration_params); if (using_exported_file()) { - //do nothing - } - else + // do nothing + } else *plate_bboxes[index] = p->generate_first_layer_bbox(); } } - //BBS: add bbs 3mf logic + // BBS: add bbs 3mf logic PlateDataPtrs plate_data_list; - p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx); + p->partplate_list.store_to_3mf_structure(plate_data_list, + (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), + export_plate_idx); // BBS: backup - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; std::vector project_presets = preset_bundle.get_current_project_embedded_presets(); StoreParams store_params; - store_params.path = path_u8; - store_params.model = &p->model; - store_params.plate_data_list = plate_data_list; - store_params.export_plate_idx = export_plate_idx; - store_params.project_presets = project_presets; - store_params.config = export_config ? &cfg : nullptr; - store_params.thumbnail_data = thumbnails; - store_params.no_light_thumbnail_data = no_light_thumbnails; - store_params.top_thumbnail_data = top_thumbnails; - store_params.pick_thumbnail_data = picking_thumbnails; + store_params.path = path_u8; + store_params.model = &p->model; + store_params.plate_data_list = plate_data_list; + store_params.export_plate_idx = export_plate_idx; + store_params.project_presets = project_presets; + store_params.config = export_config ? &cfg : nullptr; + store_params.thumbnail_data = thumbnails; + store_params.no_light_thumbnail_data = no_light_thumbnails; + store_params.top_thumbnail_data = top_thumbnails; + store_params.pick_thumbnail_data = picking_thumbnails; store_params.calibration_thumbnail_data = calibration_thumbnails; - store_params.proFn = proFn; - store_params.id_bboxes = plate_bboxes;//BBS - store_params.project = &p->project; - store_params.strategy = strategy | SaveStrategy::Zip64; - + store_params.proFn = proFn; + store_params.id_bboxes = plate_bboxes; // BBS + store_params.project = &p->project; + store_params.strategy = strategy | SaveStrategy::Zip64; // get type and color for platedata - auto* filament_color = dynamic_cast(cfg.option("filament_colour")); + auto* filament_color = dynamic_cast(cfg.option("filament_colour")); auto* nozzle_diameter_option = dynamic_cast(cfg.option("nozzle_diameter")); - auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); + auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); std::string nozzle_diameter_str; if (nozzle_diameter_option) nozzle_diameter_str = nozzle_diameter_option->serialize(); @@ -17037,14 +19049,14 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy std::string printer_model_id = preset_bundle.printers.get_edited_preset().get_printer_type(&preset_bundle); for (int i = 0; i < plate_data_list.size(); i++) { - PlateData *plate_data = plate_data_list[i]; + PlateData* plate_data = plate_data_list[i]; plate_data->printer_model_id = printer_model_id; plate_data->nozzle_diameters = nozzle_diameter_str; for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++) { std::string display_filament_type; - it->type = cfg.get_filament_type(display_filament_type, it->id); + it->type = cfg.get_filament_type(display_filament_type, it->id); it->filament_id = filament_id_opt ? filament_id_opt->get_at(it->id) : ""; - it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; + it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; // save filament info used in curr plate int index = p->partplate_list.get_curr_plate_index(); if (store_params.id_bboxes.size() > index) { @@ -17068,11 +19080,11 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy if (p->model.design_info == nullptr) { // set designInfo before export and reset after export if (wxGetApp().is_user_login()) { - p->model.design_info = std::make_shared(); - //p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickname(); + p->model.design_info = std::make_shared(); + // p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickname(); p->model.design_info->Designer = ""; p->model.design_info->DesignerUserId = wxGetApp().getAgent()->get_user_id(); - BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer = "<< ""; + BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer = " << ""; BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer_user_id = " << p->model.design_info->DesignerUserId; } } @@ -17089,15 +19101,12 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy p->set_project_filename(path); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << path; } - } - else { + } else { ret = -1; } - if (project_presets.size() > 0) - { - for (unsigned int i = 0; i < project_presets.size(); i++) - { + if (project_presets.size() > 0) { + for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } project_presets.clear(); @@ -17105,61 +19114,40 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy release_PlateData_list(plate_data_list); - for (unsigned int i = 0; i < calibration_thumbnails.size(); i++) - { - //release the data here, as it will always be generated when export + for (unsigned int i = 0; i < calibration_thumbnails.size(); i++) { + // release the data here, as it will always be generated when export calibration_thumbnails[i]->reset(); } for (unsigned int i = 0; i < no_light_thumbnails.size(); i++) { // release the data here, as it will always be generated when export no_light_thumbnails[i]->reset(); } - for (unsigned int i = 0; i < top_thumbnails.size(); i++) - { - //release the data here, as it will always be generated when export + for (unsigned int i = 0; i < top_thumbnails.size(); i++) { + // release the data here, as it will always be generated when export top_thumbnails[i]->reset(); } top_thumbnails.clear(); - for (unsigned int i = 0; i < picking_thumbnails.size(); i++) - { - //release the data here, as it will always be generated when export - picking_thumbnails[i]->reset();; + for (unsigned int i = 0; i < picking_thumbnails.size(); i++) { + // release the data here, as it will always be generated when export + picking_thumbnails[i]->reset(); + ; } picking_thumbnails.clear(); return ret; } -void Plater::publish_project() -{ - return; -} +void Plater::publish_project() { return; } +void Plater::reload_from_disk() { p->reload_from_disk(); } -void Plater::reload_from_disk() -{ - p->reload_from_disk(); -} +void Plater::replace_with_stl() { p->replace_with_stl(); } -void Plater::replace_with_stl() -{ - p->replace_with_stl(); -} +void Plater::replace_all_with_stl() { p->replace_all_with_stl(); } -void Plater::replace_all_with_stl() -{ - p->replace_all_with_stl(); -} +void Plater::reload_all_from_disk() { p->reload_all_from_disk(); } -void Plater::reload_all_from_disk() -{ - p->reload_all_from_disk(); -} - -bool Plater::has_toolpaths_to_export() const -{ - return p->preview->get_canvas3d()->has_toolpaths_to_export(); -} +bool Plater::has_toolpaths_to_export() const { return p->preview->get_canvas3d()->has_toolpaths_to_export(); } void Plater::export_toolpaths_to_obj() const { @@ -17174,13 +19162,11 @@ void Plater::export_toolpaths_to_obj() const p->preview->get_canvas3d()->export_toolpaths_to_obj(into_u8(path).c_str()); } -bool Plater::is_empty_project() { - return model().objects.empty(); -} +bool Plater::is_empty_project() { return model().objects.empty(); } bool Plater::is_multi_extruder_ams_empty() { - std::vector extruder_ams_count_str = p->config->option("extruder_ams_count", true)->values; - std::vector> extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); + std::vector extruder_ams_count_str = p->config->option("extruder_ams_count", true)->values; + std::vector> extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); for (auto extruder_ams_count : extruder_ams_counts) { for (auto iter = extruder_ams_count.begin(); iter != extruder_ams_count.end(); ++iter) { if (iter->second != 0) @@ -17190,18 +19176,28 @@ bool Plater::is_multi_extruder_ams_empty() return true; } -//BBS: add multiple plate reslice logic +// BBS: add multiple plate reslice logic void Plater::reslice() { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: enter, process_completed_with_error=%2%")%__LINE__ %p->process_completed_with_error; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(", Line %1%: enter, process_completed_with_error=%2%") % __LINE__ % + p->process_completed_with_error; // There is "invalid data" button instead "slice now" - if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) - { + if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": process_completed_with_error, return directly"); reset_gcode_toolpaths(); return; } + // A mixed filament with deleted or type-mismatched components cannot be resolved at slicing + // time. MainFrame::get_enable_slice_status() already disables the Slice button for it, but the + // Preview-tab switch, auto-slice and queued slice events reach reslice() directly, so refuse + // here too instead of letting the engine slice the broken slot as a plain filament. + if (sidebar().has_broken_mixed_filament()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": broken mixed filament detected, refuse to slice"; + return; + } + // In case SLA gizmo is in editing mode, refuse to continue // and notify user that he should leave it first. if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) @@ -17223,8 +19219,7 @@ void Plater::reslice() // get stopped. unsigned timeout_ms = 10000; if (!stop_queue(this->get_ui_job_worker(), timeout_ms)) { - BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " - << timeout_ms << " milliseconds timeout!"; + BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " << timeout_ms << " milliseconds timeout!"; return; } @@ -17239,32 +19234,33 @@ void Plater::reslice() object->sla_points_status = sla::PointsStatus::Generating; } - //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. - // bitmask of UpdateBackgroundProcessReturnState + // FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true); if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) this->p->view3D->reload_scene(false); // If the SLA processing of just a single object's supports is running, restart slicing for the whole object. this->p->background_process.set_task(PrintBase::TaskParams()); // Only restarts if the state is valid. - //BBS: jusdge the result + // BBS: jusdge the result bool result = this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: restart background,state=%2%, result=%3%")%__LINE__%state %result; - if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) - { - //BBS: add logs - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": state %1% is UPDATE_BACKGROUND_PROCESS_INVALID, can not slice") % state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(", Line %1%: restart background,state=%2%, result=%3%") % __LINE__ % state % result; + if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) { + // BBS: add logs + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(": state %1% is UPDATE_BACKGROUND_PROCESS_INVALID, can not slice") % state; p->update_fff_scene_only_shells(); return; } - if ((!result) && p->m_slice_all && (p->m_cur_slice_plate < (p->partplate_list.get_plate_count() - 1))) - { - //slice next - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": in slicing all, current plate %1% already sliced, skip to next") % p->m_cur_slice_plate ; - SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, - SlicingProcessCompletedEvent::Finished, nullptr); + if ((!result) && p->m_slice_all && (p->m_cur_slice_plate < (p->partplate_list.get_plate_count() - 1))) { + // slice next + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format(": in slicing all, current plate %1% already sliced, skip to next") % + p->m_cur_slice_plate; + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Finished, nullptr); // Post the "complete" callback message, so that it will slice the next plate soon wxQueueEvent(this, evt.Clone()); p->m_is_slicing = true; @@ -17279,32 +19275,31 @@ void Plater::reslice() bool clean_gcode_toolpaths = true; // BBS - if (p->background_process.running()) - { - //p->ready_to_slice = false; + if (p->background_process.running()) { + // p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process is running, m_is_slicing is true"); - } - else if (!p->background_process.empty() && !p->background_process.idle()) { - //p->show_action_buttons(true); - //p->ready_to_slice = true; + } else if (!p->background_process.empty() && !p->background_process.idle()) { + // p->show_action_buttons(true); + // p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process changes to not_idle, set ready_to_slice back to true"); - } - else { - //BBS: add reset logic for empty plate - PartPlate * current_plate = p->background_process.get_current_plate(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": background process changes to not_idle, set ready_to_slice back to true"); + } else { + // BBS: add reset logic for empty plate + PartPlate* current_plate = p->background_process.get_current_plate(); if (!current_plate->has_printable_instances()) { clean_gcode_toolpaths = true; current_plate->update_slice_result_valid_state(false); - } - else { + } else { clean_gcode_toolpaths = false; current_plate->update_slice_result_valid_state(true); } p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process in idle state, use previous result, clean_gcode_toolpaths=%1%")%clean_gcode_toolpaths; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": background process in idle state, use previous result, clean_gcode_toolpaths=%1%") % + clean_gcode_toolpaths; } if (clean_gcode_toolpaths) @@ -17312,7 +19307,8 @@ void Plater::reslice() p->preview->reload_print(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index(); record_slice_preset("slicing"); } @@ -17320,14 +19316,12 @@ void Plater::reslice() void Plater::record_slice_preset(std::string action) { // record slice preset - try - { + try { json j; auto printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset; if (printer_preset.is_system) { j["printer_preset_name"] = printer_preset.name; - } - else { + } else { j["printer_preset_name"] = printer_preset.config.opt_string("inherits"); } const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; @@ -17344,8 +19338,7 @@ void Plater::record_slice_preset(std::string action) auto filament_preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]); if (filament_preset->is_system) { j["filament_preset_" + std::to_string(i)] = filament_preset->name; - } - else { + } else { j["filament_preset_" + std::to_string(i)] = filament_preset->config.opt_string("inherits"); } } @@ -17353,12 +19346,12 @@ void Plater::record_slice_preset(std::string action) Preset& print_preset = wxGetApp().preset_bundle->prints.get_edited_preset(); if (print_preset.is_system) { j["process_preset"] = print_preset.name; - } - else { + } else { j["process_preset"] = print_preset.config.opt_string("inherits"); } j["support_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("support_type")); - j["sparse_infill_pattern"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("sparse_infill_pattern")); + j["sparse_infill_pattern"] = ConfigOptionEnum::get_enum_names().at( + print_preset.config.opt_enum("sparse_infill_pattern")); j["sparse_infill_density"] = print_preset.config.opt("sparse_infill_density")->value; j["brim_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("brim_type")); @@ -17366,7 +19359,7 @@ void Plater::record_slice_preset(std::string action) if (p->background_process.fff_print()) { const DynamicPrintConfig& full_config = p->background_process.fff_print()->full_print_config(); - json values = json::array(); + json values = json::array(); if (full_config.has("different_settings_to_system")) { std::vector different_values = full_config.option("different_settings_to_system")->values; for (auto& item : different_values) { @@ -17376,28 +19369,26 @@ void Plater::record_slice_preset(std::string action) j["different_settings_to_system"] = values; } - j["record_event"] = action; + j["record_event"] = action; NetworkAgent* agent = wxGetApp().getAgent(); - } - catch (...) - { + } catch (...) { return; } } -//BBS: add project slicing related logic +// BBS: add project slicing related logic int Plater::start_next_slice() { // Stop arrange and (or) optimize rotation tasks. - //this->stop_jobs(); + // this->stop_jobs(); - //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. - // bitmask of UpdateBackgroundProcessReturnState + // FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) this->p->view3D->reload_scene(false); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": update_background_process returns %1%")%state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": update_background_process returns %1%") % state; if (!p->partplate_list.get_curr_plate()->can_slice()) { p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found invalidated apply in update_background_process."); @@ -17406,34 +19397,27 @@ int Plater::start_next_slice() // Only restarts if the state is valid. bool result = this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); - if (!result) - { - //slice next - SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, - SlicingProcessCompletedEvent::Finished, nullptr); + if (!result) { + // slice next + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Finished, nullptr); // Post the "complete" callback message, so that it will slice the next plate soon wxQueueEvent(this, evt.Clone()); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": restart_background_process returns %1%")%result; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": restart_background_process returns %1%") % result; return 0; } +void Plater::reslice_SLA_supports(const ModelObject& object, bool postpone_error_messages) +{ reslice_SLA_until_step(slaposPad, object, postpone_error_messages); } -void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages) -{ - reslice_SLA_until_step(slaposPad, object, postpone_error_messages); -} +void Plater::reslice_SLA_hollowing(const ModelObject& object, bool postpone_error_messages) +{ reslice_SLA_until_step(slaposDrillHoles, object, postpone_error_messages); } -void Plater::reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages) +void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject& object, bool postpone_error_messages) { - reslice_SLA_until_step(slaposDrillHoles, object, postpone_error_messages); -} - -void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages) -{ - //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. - // bitmask of UpdateBackgroundProcessReturnState + // FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, postpone_error_messages); if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) this->p->view3D->reload_scene(false); @@ -17449,7 +19433,7 @@ void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject & // Otherwise calculate everything, but start with the provided object. if (!this->p->background_processing_enabled()) { task.single_model_instance_only = true; - task.to_object_step = step; + task.to_object_step = step; } this->p->background_process.set_task(task); // and let the background processing start. @@ -17460,7 +19444,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) // if physical_printer is selected, send gcode for this printer // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); DynamicPrintConfig* physical_printer_config = &Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - if (! physical_printer_config || p->model.objects.empty()) + if (!physical_printer_config || p->model.objects.empty()) return; PrintHostJob upload_job(physical_printer_config); @@ -17469,7 +19453,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) // Orca: the use_3mf printer option makes us send a .gcode.3mf to the printer const auto* use_3mf_opt = physical_printer_config->option("use_3mf"); - const bool use_3mf = use_3mf_opt != nullptr && use_3mf_opt->value; + const bool use_3mf = use_3mf_opt != nullptr && use_3mf_opt->value; upload_job.upload_data.use_3mf = use_3mf; // Orca: the concrete plate to export/send (PLATE_CURRENT_IDX resolves to the current plate). @@ -17483,8 +19467,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) unsigned int state = this->p->update_restart_background_process(false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) return; - default_output_file = this->p->background_process.output_filepath_for_project( - into_path(this->p->get_project_filename(".3mf"))); + default_output_file = this->p->background_process.output_filepath_for_project(into_path(this->p->get_project_filename(".3mf"))); } catch (const Slic3r::PlaceholderParserError& ex) { // Show the error with monospaced font. show_error(this, ex.what(), true); @@ -17520,8 +19503,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } { - auto preset_bundle = wxGetApp().preset_bundle; - auto config = get_app_config(); + auto preset_bundle = wxGetApp().preset_bundle; + auto config = get_app_config(); const auto host_type_opt = physical_printer_config->option>("host_type"); const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htElegooLink; @@ -17536,8 +19519,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); } else if (host_type == htCrealityPrint) { - pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, - storage_paths, storage_names, + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), + groups, storage_paths, storage_names, config->get_bool("open_device_tab_post_upload"), upload_job.printhost.get()); } else if (flashforge_local_api) { @@ -17548,10 +19531,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } std::vector slots; - bool supports_material_station = false; + bool supports_material_station = false; { wxBusyCursor wait; - wxString msg; + wxString msg; if (!flashforge_host->fetch_material_slots(slots, &supports_material_station, msg)) { show_error(this, msg.empty() ? _L("Unable to log in to the Flashforge printer.") : msg, false); return; @@ -17559,10 +19542,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } std::vector project_filaments; - PlateDataPtrs plate_data_list; - DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config(); - const auto* filament_color = dynamic_cast(cfg.option("filament_colour")); - const auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); + PlateDataPtrs plate_data_list; + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config(); + const auto* filament_color = dynamic_cast(cfg.option("filament_colour")); + const auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); auto enrich_project_filaments = [&](std::vector& filaments) { for (auto& filament : filaments) { if (filament.id < 0) @@ -17571,8 +19554,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) std::string display_filament_type; try { filament.type = cfg.get_filament_type(display_filament_type, filament.id); - } catch (...) { - } + } catch (...) {} if (filament.type.empty()) filament.type = display_filament_type; @@ -17587,7 +19569,9 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) }; p->partplate_list.store_to_3mf_structure(plate_data_list, true, plate_idx); - PlateData* selected_plate_data = (resolved_plate_idx >= 0 && resolved_plate_idx < static_cast(plate_data_list.size())) ? plate_data_list[resolved_plate_idx] : nullptr; + PlateData* selected_plate_data = (resolved_plate_idx >= 0 && resolved_plate_idx < static_cast(plate_data_list.size())) ? + plate_data_list[resolved_plate_idx] : + nullptr; if (selected_plate_data == nullptr && !plate_data_list.empty()) selected_plate_data = plate_data_list.front(); @@ -17603,13 +19587,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) enrich_project_filaments(project_filaments); release_PlateData_list(plate_data_list); - pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, - storage_paths, storage_names, - config->get_bool("open_device_tab_post_upload"), - flashforge_host, - supports_material_station, - std::move(slots), - project_filaments); + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), + groups, storage_paths, storage_names, + config->get_bool("open_device_tab_post_upload"), flashforge_host, + supports_material_station, std::move(slots), project_filaments); } else { pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); @@ -17622,11 +19603,11 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) config->set_bool("open_device_tab_post_upload", pDlg->switch_to_device_tab()); // PrintHostUpload upload_data; - upload_job.switch_to_device_tab = pDlg->switch_to_device_tab(); - upload_job.upload_data.upload_path = pDlg->filename(); - upload_job.upload_data.post_action = pDlg->post_action(); - upload_job.upload_data.group = pDlg->group(); - upload_job.upload_data.storage = pDlg->storage(); + upload_job.switch_to_device_tab = pDlg->switch_to_device_tab(); + upload_job.upload_data.upload_path = pDlg->filename(); + upload_job.upload_data.post_action = pDlg->post_action(); + upload_job.upload_data.group = pDlg->group(); + upload_job.upload_data.storage = pDlg->storage(); upload_job.upload_data.extended_info = pDlg->extendedInfo(); // Orca: gcode inside a .gcode.3mf is index-coded (Metadata/plate_.gcode) and a bundle may // carry several of them, so the upload must name which plate to print via a 1-based plateindex. @@ -17640,8 +19621,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } // Show "Is printer clean" dialog for PrusaConnect - Upload and print. - if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { - GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), wxOK | wxCANCEL); + if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && + upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), + wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) return; } @@ -17671,15 +19654,14 @@ int Plater::send_gcode(int plate_idx, Export3mfProgressFn proFn) try { p->m_print_job_data._3mf_path = fs::path(plate->get_tmp_gcode_path()); p->m_print_job_data._3mf_path.replace_extension("3mf"); - } - catch (std::exception&) { + } catch (std::exception&) { BOOST_LOG_TRIVIAL(error) << "generate 3mf path failed"; return -1; } SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SkipModel | SaveStrategy::WithGcode | SaveStrategy::SkipAuxiliary; #if !BBL_RELEASE_TO_PUBLIC - //only save model in QA environment + // only save model in QA environment std::string sel = get_app_config()->get("iot_environment"); if (sel == ENV_PRE_HOST) strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode; @@ -17699,20 +19681,19 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) PartPlate* plate = get_partplate_list().get_curr_plate(); try { p->m_print_job_data._3mf_config_path = fs::path(plate->get_temp_config_3mf_path()); - } - catch (std::exception&) { + } catch (std::exception&) { BOOST_LOG_TRIVIAL(error) << "generate 3mf path failed"; return -1; } SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SkipModel | SaveStrategy::WithSliceInfo | SaveStrategy::SkipAuxiliary; - result = export_3mf(p->m_print_job_data._3mf_config_path, strategy, plate_idx, proFn); + result = export_3mf(p->m_print_job_data._3mf_config_path, strategy, plate_idx, proFn); return result; } -//BBS -void Plater::send_calibration_job_finished(wxCommandEvent & evt) +// BBS +void Plater::send_calibration_job_finished(wxCommandEvent& evt) { p->main_frame->request_select_tab(TAB_ID_CALIBRATION); auto calibration_panel = p->main_frame->m_calibration; @@ -17726,9 +19707,9 @@ void Plater::send_calibration_job_finished(wxCommandEvent & evt) evt.Skip(); } -void Plater::print_job_finished(wxCommandEvent &evt) +void Plater::print_job_finished(wxCommandEvent& evt) { - //start print failed + // start print failed if (p) { #ifdef __APPLE__ p->hide_select_machine_dlg(); @@ -17741,63 +19722,60 @@ void Plater::print_job_finished(wxCommandEvent &evt) #endif // __APPLE__ } - Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) return; + if (!dev) + return; dev->set_selected_machine(evt.GetString().ToStdString()); p->main_frame->request_select_tab(TAB_ID_MONITOR); - //jump to monitor and select device status panel + // jump to monitor and select device status panel MonitorPanel* curr_monitor = p->main_frame->m_monitor; - if(curr_monitor) - curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); + if (curr_monitor) + curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); } void Plater::send_job_finished(wxCommandEvent& evt) { Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) return; - //dev->set_selected_machine(evt.GetString().ToStdString()); + if (!dev) + return; + // dev->set_selected_machine(evt.GetString().ToStdString()); send_gcode_finish(evt.GetString()); p->hide_send_to_printer_dlg(); - //p->main_frame->request_select_tab(TAB_ID_MONITOR); + // p->main_frame->request_select_tab(TAB_ID_MONITOR); ////jump to monitor and select device status panel - //MonitorPanel* curr_monitor = p->main_frame->m_monitor; - //if (curr_monitor) - // curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); + // MonitorPanel* curr_monitor = p->main_frame->m_monitor; + // if (curr_monitor) + // curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); } -void Plater::publish_job_finished(wxCommandEvent &evt) +void Plater::publish_job_finished(wxCommandEvent& evt) { p->m_publish_dlg->EndModal(wxID_OK); - // GUI::wxGetApp().load_url(evt.GetString()); - //GUI::wxGetApp().open_publish_page_dialog(evt.GetString()); + // GUI::wxGetApp().load_url(evt.GetString()); + // GUI::wxGetApp().open_publish_page_dialog(evt.GetString()); } // Called when the Eject button is pressed. void Plater::eject_drive() { - wxBusyCursor wait; + wxBusyCursor wait; wxGetApp().removable_drive_manager()->set_and_verify_last_save_path(p->last_output_dir_path); - wxGetApp().removable_drive_manager()->eject_drive(); + wxGetApp().removable_drive_manager()->eject_drive(); } -void Plater::take_snapshot(const std::string &snapshot_name) { p->take_snapshot(snapshot_name); } -//void Plater::take_snapshot(const wxString &snapshot_name) { p->take_snapshot(snapshot_name); } -void Plater::take_snapshot(const std::string &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); } -//void Plater::take_snapshot(const wxString &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); } +void Plater::take_snapshot(const std::string& snapshot_name) { p->take_snapshot(snapshot_name); } +// void Plater::take_snapshot(const wxString &snapshot_name) { p->take_snapshot(snapshot_name); } +void Plater::take_snapshot(const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type) +{ p->take_snapshot(snapshot_name, snapshot_type); } +// void Plater::take_snapshot(const wxString &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, +// snapshot_type); } void Plater::suppress_snapshots() { p->suppress_snapshots(); } void Plater::allow_snapshots() { p->allow_snapshots(); } // BBS: single snapshot -void Plater::single_snapshots_enter(SingleSnapshot *single) -{ - p->single_snapshots_enter(single); -} -void Plater::single_snapshots_leave(SingleSnapshot *single) -{ - p->single_snapshots_leave(single); -} +void Plater::single_snapshots_enter(SingleSnapshot* single) { p->single_snapshots_enter(single); } +void Plater::single_snapshots_leave(SingleSnapshot* single) { p->single_snapshots_leave(single); } void Plater::undo() { p->undo(); } void Plater::redo() { p->redo(); } void Plater::undo_to(int selection) @@ -17823,9 +19801,9 @@ void Plater::redo_to(int selection) bool Plater::undo_redo_string_getter(const bool is_undo, int idx, const char** out_text) { const std::vector& ss_stack = p->undo_redo_stack().snapshots(); - const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -(++idx) : idx); + const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -(++idx) : idx); - if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) { + if (0 < idx_in_ss_stack && (size_t) idx_in_ss_stack < ss_stack.size() - 1) { *out_text = ss_stack[idx_in_ss_stack].name.c_str(); return true; } @@ -17833,18 +19811,19 @@ bool Plater::undo_redo_string_getter(const bool is_undo, int idx, const char** o return false; } -int Plater::update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path) -{ - return p->update_print_required_data(config, model, plate_data_list, file_name, file_path); -} - +int Plater::update_print_required_data(Slic3r::DynamicPrintConfig config, + Slic3r::Model model, + Slic3r::PlateDataPtrs plate_data_list, + std::string file_name, + std::string file_path) +{ return p->update_print_required_data(config, model, plate_data_list, file_name, file_path); } void Plater::undo_redo_topmost_string_getter(const bool is_undo, std::string& out_text) { const std::vector& ss_stack = p->undo_redo_stack().snapshots(); - const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -1 : 0); + const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -1 : 0); - if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) { + if (0 < idx_in_ss_stack && (size_t) idx_in_ss_stack < ss_stack.size() - 1) { out_text = ss_stack[idx_in_ss_stack].name; return; } @@ -17856,7 +19835,7 @@ bool Plater::search_string_getter(int idx, const char** label, const char** tool { const Search::OptionsSearcher& search_list = p->sidebar->get_searcher(); - if (0 <= idx && (size_t)idx < search_list.size()) { + if (0 <= idx && (size_t) idx < search_list.size()) { search_list[idx].get_marked_label_and_tooltip(label, tooltip); return true; } @@ -17883,7 +19862,7 @@ void Plater::on_filament_count_change(size_t num_filaments) sidebar().on_filament_count_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); - Slic3r::GUI::PartPlateList &plate_list = get_partplate_list(); + Slic3r::GUI::PartPlateList& plate_list = get_partplate_list(); plate_list.set_filament_count(num_filaments); for (int i = 0; i < plate_list.get_plate_count(); ++i) { PartPlate* part_plate = plate_list.get_plate(i); @@ -17897,13 +19876,16 @@ void Plater::on_filament_count_change(size_t num_filaments) } } -void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) +void Plater::on_filaments_delete(size_t num_filaments, + size_t filament_id, + int replace_filament_id, + const std::vector& is_mixed_before_delete) { // only update elements in plater update_filament_colors_in_full_config(); // update fisrt print sequence and other layer sequence - //move to partplate->on_filament_deleted + // move to partplate->on_filament_deleted /*Slic3r::GUI::PartPlateList &plate_list = get_partplate_list(); for (int i = 0; i < plate_list.get_plate_count(); ++i) { PartPlate *part_plate = plate_list.get_plate(i); @@ -17911,20 +19893,29 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r }*/ // update mmu info - for (ModelObject *mo : wxGetApp().model().objects) { - for (ModelVolume *mv : mo->volumes) { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1); // this function is 1 base + // A volume assigned to a mixed slot legitimately sits past the physical filament count, so + // the paint cleanup must know which slots were mixed. Callers that already shrank the arrays + // pass the pre-delete flags; otherwise read the current ones. + const auto& is_mixed = is_mixed_before_delete.empty() ? + wxGetApp().preset_bundle->project_config.option("filament_is_mixed")->values : + is_mixed_before_delete; + for (ModelObject* mo : wxGetApp().model().objects) { + for (ModelVolume* mv : mo->volumes) { + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1, + is_mixed); // this function is 1 base } } - // update UI - sidebar().on_filaments_delete(filament_id); + // update object/volume/support(object and volume) filament id + // Must run before UI update which triggers update_mixed_filament_list() → + // update_objects_list_filament_column() that clips extruders above total count. + sidebar().obj_list()->update_objects_list_filament_column_when_delete_filament(filament_id, num_filaments, replace_filament_id); // update global support filament - static const char *keys[] = {"support_filament", "support_interface_filament"}; + static const char* keys[] = {"support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { - if(p->config->opt_int(key) == filament_id + 1) + if (p->config->opt_int(key) == filament_id + 1) (*(p->config)).erase(key); else { int new_value = p->config->opt_int(key) > filament_id ? p->config->opt_int(key) - 1 : p->config->opt_int(key); @@ -17932,8 +19923,8 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r } } - // update object/volume/support(object and volume) filament id - sidebar().obj_list()->update_objects_list_filament_column_when_delete_filament(filament_id, num_filaments, replace_filament_id); + // update UI — runs after remap so update_mixed_filament_list() won't clip remapped extruder IDs + sidebar().on_filaments_delete(filament_id); // update customize gcode for (auto item = p->model.plates_custom_gcodes.begin(); item != p->model.plates_custom_gcodes.end(); ++item) { @@ -17942,7 +19933,7 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r }); if (replace_filament_id == -1) item->second.gcodes.erase(iter, item->second.gcodes.end()); - else if(iter != item->second.gcodes.end()) { + else if (iter != item->second.gcodes.end()) { iter->extruder = replace_filament_id + 1; } @@ -17955,10 +19946,10 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r std::vector Plater::get_extruders_colors() { - unsigned char rgba_color[4] = {}; - std::vector colors = get_extruder_colors_from_plater_config(); + unsigned char rgba_color[4] = {}; + std::vector colors = get_extruder_colors_from_plater_config(); std::vector colors_out(colors.size()); - for (const std::string &color : colors) { + for (const std::string& color : colors) { Slic3r::GUI::BitmapCache::parse_color4(color, rgba_color); size_t color_idx = &color - &colors.front(); colors_out[color_idx] = { @@ -17971,24 +19962,21 @@ std::vector Plater::get_extruders_colors() return colors_out; } -void Plater::on_bed_type_change(BedType bed_type) -{ - sidebar().on_bed_type_change(bed_type); -} +void Plater::on_bed_type_change(BedType bed_type) { sidebar().on_bed_type_change(bed_type); } bool Plater::update_filament_colors_in_full_config() { - DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; - const auto& full_config = wxGetApp().preset_bundle->full_config(); - ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); const ConfigOptionStrings* type_opt = full_config.option("filament_type"); p->config->option("filament_colour")->values = color_opt->values; - p->config->option("filament_type")->values = type_opt->values; + p->config->option("filament_type")->values = type_opt->values; return true; } -void Plater::config_change_notification(const DynamicPrintConfig &config, const std::string& key) +void Plater::config_change_notification(const DynamicPrintConfig& config, const std::string& key) { GLCanvas3D* view3d_canvas = get_view3D_canvas3D(); if (key == std::string("print_sequence")) { @@ -17998,24 +19986,23 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const if (seq_print->value == PrintSequence::ByObject) { std::string info_text = _u8L("Print By Object: \nWe suggest using auto-arrange to avoid collisions when printing."); notify_manager->bbl_show_seqprintinfo_notification(info_text); - } - else + } else notify_manager->bbl_close_seqprintinfo_notification(); } } // notification for more options } -void Plater::on_config_change(const DynamicPrintConfig &config) +void Plater::on_config_change(const DynamicPrintConfig& config) { - bool update_scheduled = false; + bool update_scheduled = false; bool bed_shape_changed = false; - //bool print_sequence_changed = false; + // bool print_sequence_changed = false; t_config_option_keys diff_keys = p->config->diff(config); size_t old_nozzle_size = 1, new_nozzle_size = 1; - auto * opt_old = p->config->option("nozzle_diameter"); - auto * opt_new = config.option("nozzle_diameter"); + auto* opt_old = p->config->option("nozzle_diameter"); + auto* opt_new = config.option("nozzle_diameter"); if (opt_old && opt_new) { old_nozzle_size = opt_old->values.size(); new_nozzle_size = opt_new->values.size(); @@ -18026,6 +20013,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_scheduled = true; // update should be scheduled (for update 3DScene) #2738 if (update_filament_colors_in_full_config()) { + p->sidebar->update_mixed_filament_list(); p->sidebar->obj_list()->update_filament_colors(); p->sidebar->update_dynamic_filament_list(); continue; @@ -18033,6 +20021,15 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } if (opt_key == "filament_type") { update_filament_colors_in_full_config(); + p->sidebar->update_mixed_filament_list(); + continue; + } + // The mixed-filament type check folds filament_is_support into the component type + // (DynamicPrintConfig::get_filament_type -> "PLA-S"), so a support-preset switch must + // refresh the list even though filament_type itself did not change. + if (opt_key == "filament_is_support") { + p->config->set_key_value(opt_key, config.option(opt_key)->clone()); + p->sidebar->update_mixed_filament_list(); continue; } if (opt_key == "material_colour") { @@ -18047,43 +20044,34 @@ void Plater::on_config_change(const DynamicPrintConfig &config) p->reset_gcode_toolpaths(); p->view3D->get_canvas3d()->reset_sequential_print_clearance(); p->preview->get_canvas3d()->reset_volumes(); - //BBS: invalid all the slice results + // BBS: invalid all the slice results p->partplate_list.invalid_all_slice_result(); } - //BBS: add bed_exclude_area - else if (opt_key == "printable_area" || opt_key == "bed_exclude_area" - || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model" - || opt_key == "extruder_clearance_height_to_lid" - || opt_key == "extruder_clearance_height_to_rod") { + // BBS: add bed_exclude_area + else if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "bed_custom_texture" || + opt_key == "bed_custom_model" || opt_key == "extruder_clearance_height_to_lid" || + opt_key == "extruder_clearance_height_to_rod") { bed_shape_changed = true; - update_scheduled = true; - } - else if (opt_key == "bed_shape" || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model") { + update_scheduled = true; + } else if (opt_key == "bed_shape" || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model") { bed_shape_changed = true; + update_scheduled = true; + } else if (boost::starts_with(opt_key, "enable_prime_tower") || boost::starts_with(opt_key, "prime_tower") || + boost::starts_with(opt_key, "wipe_tower") || opt_key == "filament_minimal_purge_on_wipe_tower" || + opt_key == "single_extruder_multi_material" || + // BBS + opt_key == "prime_volume") { update_scheduled = true; - } - else if (boost::starts_with(opt_key, "enable_prime_tower") || - boost::starts_with(opt_key, "prime_tower") || - boost::starts_with(opt_key, "wipe_tower") || - opt_key == "filament_minimal_purge_on_wipe_tower" || - opt_key == "single_extruder_multi_material" || - // BBS - opt_key == "prime_volume") { + } else if (opt_key == "extruder_colour") { update_scheduled = true; - } - else if(opt_key == "extruder_colour") { - update_scheduled = true; - //p->sidebar->obj_list()->update_extruder_colors(); - } - else if (opt_key == "printable_height") { + // p->sidebar->obj_list()->update_extruder_colors(); + } else if (opt_key == "printable_height") { bed_shape_changed = true; + update_scheduled = true; + } else if (opt_key == "print_sequence") { update_scheduled = true; - } - else if (opt_key == "print_sequence") { - update_scheduled = true; - //print_sequence_changed = true; - } - else if (opt_key == "printer_model") { + // print_sequence_changed = true; + } else if (opt_key == "printer_model") { p->reset_gcode_toolpaths(); if (old_nozzle_size != new_nozzle_size) { update_flush_volume_matrix(old_nozzle_size, new_nozzle_size); @@ -18091,12 +20079,11 @@ void Plater::on_config_change(const DynamicPrintConfig &config) // update to force bed selection(for texturing) bed_shape_changed = true; - update_scheduled = true; + update_scheduled = true; } // Orca: update when *_filament changed - else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || - opt_key == "outer_wall_filament_id" || opt_key == "inner_wall_filament_id" || - opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" || + else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "outer_wall_filament_id" || + opt_key == "inner_wall_filament_id" || opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" || opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") { update_scheduled = true; } @@ -18119,42 +20106,45 @@ void Plater::on_config_change(const DynamicPrintConfig &config) void Plater::update_flush_volume_matrix(size_t old_nozzle_size, size_t new_nozzle_size) { - size_t nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - Slic3r::DynamicPrintConfig *project_config = &wxGetApp().preset_bundle->project_config; + size_t nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + Slic3r::DynamicPrintConfig* project_config = &wxGetApp().preset_bundle->project_config; // Verify whether it is the first time start Studio - size_t filament_nums = project_config->option("filament_colour")->values.size(); + size_t filament_nums = project_config->option("filament_colour")->values.size(); size_t flush_volume_size = project_config->option("flush_volumes_matrix")->values.size(); assert(nozzle_nums == new_nozzle_size); if (old_nozzle_size < new_nozzle_size) { - - std::vector first_flush_volume_mtx = get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); + std::vector first_flush_volume_mtx = + get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); if (first_flush_volume_mtx.size() == filament_nums * filament_nums * new_nozzle_size) { // load file - set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, first_flush_volume_mtx, -1, new_nozzle_size); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, first_flush_volume_mtx, -1, + new_nozzle_size); } else { first_flush_volume_mtx.resize(filament_nums * filament_nums, 0); std::vector flush_volume_mtx; for (size_t i = 0; i < new_nozzle_size; ++i) { flush_volume_mtx.insert(flush_volume_mtx.end(), first_flush_volume_mtx.begin(), first_flush_volume_mtx.end()); } - set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, flush_volume_mtx, -1, new_nozzle_size); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, flush_volume_mtx, -1, + new_nozzle_size); } std::vector flush_multipliers = project_config->option("flush_multiplier")->values; flush_multipliers.resize(nozzle_nums, 1.f); project_config->option("flush_multiplier")->values = flush_multipliers; - } - else if (old_nozzle_size > new_nozzle_size) { + } else if (old_nozzle_size > new_nozzle_size) { std::vector new_flush_volume_mtx; for (size_t i = 0; i < new_nozzle_size; ++i) { - std::vector flush_volume_mtx = get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); + std::vector flush_volume_mtx = + get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); flush_volume_mtx.resize(filament_nums * filament_nums, 0); new_flush_volume_mtx.insert(new_flush_volume_mtx.end(), flush_volume_mtx.begin(), flush_volume_mtx.end()); } std::vector flush_multipliers = project_config->option("flush_multiplier")->values; flush_multipliers.resize(nozzle_nums, 1.f); - set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, new_flush_volume_mtx, -1, new_nozzle_size); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, new_flush_volume_mtx, -1, + new_nozzle_size); project_config->option("flush_multiplier")->values = flush_multipliers; } } @@ -18168,32 +20158,43 @@ void Plater::set_bed_shape() const if (curr->is_system) texture_filename = PresetUtils::system_printer_bed_texture(*curr); else { - auto *printer_model = curr->config.opt("printer_model"); - if (printer_model != nullptr && ! printer_model->value.empty()) { + auto* printer_model = curr->config.opt("printer_model"); + if (printer_model != nullptr && !printer_model->value.empty()) { texture_filename = bundle->get_texture_for_printer_model(printer_model->value); } } } set_bed_shape(p->config->option("printable_area")->values, - //BBS: add bed exclude areas - p->config->option("bed_exclude_area")->values, - p->config->option("wrapping_exclude_area")->values, - p->config->option("printable_height")->value, - p->config->option("extruder_printable_area")->values, - p->config->option("extruder_printable_height")->values, - p->config->option("bed_custom_texture")->value.empty() ? texture_filename : p->config->option("bed_custom_texture")->value, - p->config->option("bed_custom_model")->value); + // BBS: add bed exclude areas + p->config->option("bed_exclude_area")->values, + p->config->option("wrapping_exclude_area")->values, + p->config->option("printable_height")->value, + p->config->option("extruder_printable_area")->values, + p->config->option("extruder_printable_height")->values, + p->config->option("bed_custom_texture")->value.empty() ? + texture_filename : + p->config->option("bed_custom_texture")->value, + p->config->option("bed_custom_model")->value); } -//BBS: add bed exclude area -void Plater::set_bed_shape(const Pointfs& shape, const Pointfs& exclude_area, const Pointfs& wrapping_exclude_area, const double printable_height, std::vector extruder_areas, std::vector extruder_heights, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) const +// BBS: add bed exclude area +void Plater::set_bed_shape(const Pointfs& shape, + const Pointfs& exclude_area, + const Pointfs& wrapping_exclude_area, + const double printable_height, + std::vector extruder_areas, + std::vector extruder_heights, + const std::string& custom_texture, + const std::string& custom_model, + bool force_as_custom) const { - p->set_bed_shape(make_counter_clockwise(shape), exclude_area, wrapping_exclude_area, printable_height, extruder_areas, extruder_heights, custom_texture, custom_model, force_as_custom); + p->set_bed_shape(make_counter_clockwise(shape), exclude_area, wrapping_exclude_area, printable_height, extruder_areas, extruder_heights, + custom_texture, custom_model, force_as_custom); } void Plater::force_filament_colors_update() { -//BBS: filament_color logic has been moved out of filament setting +// BBS: filament_color logic has been moved out of filament setting #if 0 bool update_scheduled = false; DynamicPrintConfig* config = p->config; @@ -18226,15 +20227,12 @@ void Plater::force_filament_colors_update() void Plater::force_print_bed_update() { - // Fill in the printer model key with something which cannot possibly be valid, so that Plater::on_config_change() will update the print bed - // once a new Printer profile config is loaded. + // Fill in the printer model key with something which cannot possibly be valid, so that Plater::on_config_change() will update the print + // bed once a new Printer profile config is loaded. p->config->opt_string("printer_model", true) = "bbl_empty"; } -void Plater::on_activate() -{ - this->p->show_delayed_error_message(); -} +void Plater::on_activate() { this->p->show_delayed_error_message(); } // Get vector of extruder colors considering filament color, if extruder color is undefined. std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const @@ -18252,26 +20250,138 @@ std::vector Plater::get_extruder_colors_from_plater_config(const GC } } +namespace { + +// A gradient mixed filament fades between its two components over Z, so the UI shows it as a +// two-tone swatch rather than one blended colour. Resolve each slot to its from/to endpoint +// colours; non-gradient slots are left untouched. +struct MixedGradientSlot +{ + bool is_gradient = false; + std::string color_from; + std::string color_to; +}; + +std::vector parse_mixed_gradient_slots(const Slic3r::DynamicPrintConfig& config, size_t slot_count) +{ + std::vector result(slot_count); + const auto* is_mixed = config.option("filament_is_mixed"); + const auto* mixed_grad = config.option("filament_mixed_gradient"); + const auto* mixed_comp = config.option("filament_mixed_components"); + const auto* grad_range = config.option("filament_mixed_gradient_range"); + const auto* fil_colour = config.option("filament_colour"); + if (!is_mixed || !mixed_grad || !mixed_comp || !fil_colour) + return result; + + for (size_t i = 0; i < slot_count && i < is_mixed->values.size(); ++i) { + if (!is_mixed->values[i]) + continue; + if (i >= mixed_grad->values.size() || !mixed_grad->values[i]) + continue; + if (i >= mixed_comp->values.size()) + continue; + + std::vector comp_ids; + std::istringstream iss(mixed_comp->values[i]); + std::string tok; + while (std::getline(iss, tok, ',')) { + unsigned int v = 0; + if (std::sscanf(tok.c_str(), "%u", &v) == 1) + comp_ids.push_back(v); + } + if (comp_ids.size() != 2) + continue; + + int direction = 0; + if (grad_range && i < grad_range->values.size()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(grad_range->values[i].c_str(), "%f,%f", &v0, &v1) == 2) + direction = (v0 > v1) ? 0 : 1; + } + + unsigned int from_id = (direction == 0) ? comp_ids[0] : comp_ids[1]; + unsigned int to_id = (direction == 0) ? comp_ids[1] : comp_ids[0]; + result[i].is_gradient = true; + result[i].color_from = (from_id >= 1 && from_id <= fil_colour->values.size()) ? fil_colour->values[from_id - 1] : "#D9D9D9"; + result[i].color_to = (to_id >= 1 && to_id <= fil_colour->values.size()) ? fil_colour->values[to_id - 1] : "#D9D9D9"; + } + return result; +} + +} // anonymous namespace + std::vector Plater::get_filament_colors_render_info() const { const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; std::vector color_packs; - if (!config->has("filament_multi_colour")) return color_packs; + if (!config->has("filament_multi_colour")) + return color_packs; color_packs = (config->option("filament_multi_colour"))->values; + + auto slots = parse_mixed_gradient_slots(*config, color_packs.size()); + for (size_t i = 0; i < color_packs.size(); ++i) { + if (slots[i].is_gradient) + color_packs[i] = slots[i].color_from + " " + slots[i].color_to; + } + return color_packs; } std::vector Plater::get_filament_color_render_type() const { - const Slic3r::DynamicPrintConfig *config = &wxGetApp().preset_bundle->project_config; - std::vector ctype; - if (!config->has("filament_colour_type")) return ctype; + const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; + std::vector ctype; + if (!config->has("filament_colour_type")) + return ctype; ctype = (config->option("filament_colour_type"))->values; + + auto slots = parse_mixed_gradient_slots(*config, ctype.size()); + while (ctype.size() < slots.size()) + ctype.push_back("1"); + for (size_t i = 0; i < ctype.size() && i < slots.size(); ++i) { + if (slots[i].is_gradient) + ctype[i] = "0"; + } + return ctype; } +const std::vector>& Plater::get_filament_gradient_ramps() const +{ + // Sampling a ramp walks the measured-blend recipe table once per step and the paint toolbar + // asks for the ramps every rendered frame, so they are cached against the config values they + // are built from. The cache is static rather than a Plater member because the extruder icons + // ask for the ramps from MenuFactory::init(), which runs while this Plater is still inside its + // own constructor, so wxGetApp().plater_ is not assigned yet. + static std::string s_ramps_key; + static std::vector> s_ramps; + + static const char* ramp_keys[] = {"filament_is_mixed", "filament_mixed_gradient", "filament_mixed_components", + "filament_colour", "filament_mixed_gradient_range", "filament_mixed_gradient_curve"}; + + const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; + std::string key; + for (const char* opt_key : ramp_keys) + if (const ConfigOption* opt = config.option(opt_key)) + key += opt->serialize() + '\n'; + if (key == s_ramps_key) + return s_ramps; + + // 64 bands outresolve every swatch drawn from this, all of which resample it down to their + // own height, so one cached resolution serves the icons and both ImGui filament bars. + const auto* colour_opt = config.option("filament_colour"); + const size_t n = colour_opt ? colour_opt->values.size() : 0; + s_ramps.assign(n, {}); + for (size_t i = 0; i < n; ++i) + s_ramps[i] = mixed_gradient_ramp(config, i, 64); + s_ramps_key = std::move(key); + + return s_ramps; +} + /* Get vector of colors used for rendering of a Preview scene in "Color print" mode * It consists of extruder colors and colors, saved in model.custom_gcode_per_print_z */ @@ -18284,9 +20394,8 @@ std::vector Plater::get_colors_for_color_print(const GCodeProcessor if (code.type == CustomGCode::ColorChange) colors.emplace_back(code.color); } - } - else { - //BBS + } else { + // BBS colors.reserve(colors.size() + p->model.get_curr_plate_custom_gcodes().gcodes.size()); for (const CustomGCode::Item& code : p->model.get_curr_plate_custom_gcodes().gcodes) { if (code.type == CustomGCode::ColorChange) @@ -18299,23 +20408,23 @@ std::vector Plater::get_colors_for_color_print(const GCodeProcessor void Plater::set_global_filament_map_mode(FilamentMapMode mode) { - auto& project_config = wxGetApp().preset_bundle->project_config; - auto mode_ptr = project_config.option>("filament_map_mode"); + auto& project_config = wxGetApp().preset_bundle->project_config; + auto mode_ptr = project_config.option>("filament_map_mode"); FilamentMapMode old_mode = mode_ptr->value; - if(mode != old_mode) + if (mode != old_mode) on_filament_map_mode_change(); mode_ptr->value = mode; } void Plater::set_global_filament_map(const std::vector& filament_map) { - auto& project_config = wxGetApp().preset_bundle->project_config; + auto& project_config = wxGetApp().preset_bundle->project_config; project_config.option("filament_map")->values = filament_map; } void Plater::set_global_filament_volume_map(const std::vector& filament_volume_map) { - auto& project_config = wxGetApp().preset_bundle->project_config; + auto& project_config = wxGetApp().preset_bundle->project_config; project_config.option("filament_volume_map")->values = filament_volume_map; } @@ -18331,7 +20440,6 @@ std::vector Plater::get_global_filament_volume_map() const return project_config.option("filament_volume_map")->values; } - FilamentMapMode Plater::get_global_filament_map_mode() const { auto& project_config = wxGetApp().preset_bundle->project_config; @@ -18341,19 +20449,16 @@ FilamentMapMode Plater::get_global_filament_map_mode() const void Plater::on_filament_map_mode_change() { auto& plate_list = this->get_partplate_list(); - int plate_count = plate_list.get_plate_count(); + int plate_count = plate_list.get_plate_count(); for (int idx = 0; idx < plate_count; ++idx) { - auto plate=plate_list.get_plate(idx); + auto plate = plate_list.get_plate(idx); auto plate_map_mode = plate->get_filament_map_mode(); if (plate_map_mode == fmmDefault) plate->clear_filament_map(); } } -wxWindow* Plater::get_select_machine_dialog() -{ - return p->m_select_machine_dlg; -} +wxWindow* Plater::get_select_machine_dialog() { return p->m_select_machine_dlg; } void Plater::update_print_error_info(int code, std::string msg, std::string extra) { @@ -18368,40 +20473,20 @@ void Plater::update_print_error_info(int code, std::string msg, std::string extr p->main_frame->m_calibration->update_print_error_info(code, msg, extra); } -wxString Plater::get_project_filename(const wxString& extension) const -{ - return p->get_project_filename(extension); -} +wxString Plater::get_project_filename(const wxString& extension) const { return p->get_project_filename(extension); } -wxString Plater::get_export_gcode_filename(const wxString & extension, bool only_filename, bool export_all) const -{ - return p->get_export_gcode_filename(extension, only_filename, export_all); -} +wxString Plater::get_export_gcode_filename(const wxString& extension, bool only_filename, bool export_all) const +{ return p->get_export_gcode_filename(extension, only_filename, export_all); } -void Plater::set_project_filename(const wxString& filename) -{ - p->set_project_filename(filename); -} +void Plater::set_project_filename(const wxString& filename) { p->set_project_filename(filename); } -bool Plater::is_export_gcode_scheduled() const -{ - return p->background_process.is_export_scheduled(); -} +bool Plater::is_export_gcode_scheduled() const { return p->background_process.is_export_scheduled(); } -const Selection &Plater::get_selection() const -{ - return p->get_selection(); -} +const Selection& Plater::get_selection() const { return p->get_selection(); } -int Plater::get_selected_object_idx() -{ - return p->get_selected_object_idx(); -} +int Plater::get_selected_object_idx() { return p->get_selected_object_idx(); } -bool Plater::is_single_full_object_selection() const -{ - return p->get_selection().is_single_full_object(); -} +bool Plater::is_single_full_object_selection() const { return p->get_selection().is_single_full_object(); } GLCanvas3D* Plater::canvas3D() { @@ -18415,15 +20500,9 @@ const GLCanvas3D* Plater::canvas3D() const return p->get_current_canvas3D(); } -GLCanvas3D* Plater::get_view3D_canvas3D() -{ - return p ? p->view3D->get_canvas3d() : nullptr; -} +GLCanvas3D* Plater::get_view3D_canvas3D() { return p ? p->view3D->get_canvas3d() : nullptr; } -GLCanvas3D* Plater::get_preview_canvas3D() -{ - return p->preview->get_canvas3d(); -} +GLCanvas3D* Plater::get_preview_canvas3D() { return p->preview->get_canvas3d(); } GLCanvas3D* Plater::get_assmeble_canvas3D() { @@ -18432,51 +20511,36 @@ GLCanvas3D* Plater::get_assmeble_canvas3D() return nullptr; } -GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) -{ - return p->get_current_canvas3D(exclude_preview); -} +GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) { return p->get_current_canvas3D(exclude_preview); } void Plater::arrange() { - auto &w = get_ui_job_worker(); + auto& w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Arrange")); replace_job(w, std::make_unique()); } } -void Plater::set_current_canvas_as_dirty() -{ - p->set_current_canvas_as_dirty(); -} +void Plater::set_current_canvas_as_dirty() { p->set_current_canvas_as_dirty(); } -void Plater::unbind_canvas_event_handlers() -{ - p->unbind_canvas_event_handlers(); -} +void Plater::unbind_canvas_event_handlers() { p->unbind_canvas_event_handlers(); } -void Plater::reset_canvas_volumes() -{ - p->reset_canvas_volumes(); -} +void Plater::reset_canvas_volumes() { p->reset_canvas_volumes(); } -PrinterTechnology Plater::printer_technology() const -{ - return p->printer_technology; -} +PrinterTechnology Plater::printer_technology() const { return p->printer_technology; } -const DynamicPrintConfig * Plater::config() const { return p->config; } +const DynamicPrintConfig* Plater::config() const { return p->config; } bool Plater::set_printer_technology(PrinterTechnology printer_technology) { p->printer_technology = printer_technology; - bool ret = p->background_process.select_technology(printer_technology); + bool ret = p->background_process.select_technology(printer_technology); if (ret) { // Update the active presets. } - //FIXME for SLA synchronize - //p->background_process.apply(Model)! + // FIXME for SLA synchronize + // p->background_process.apply(Model)! if (printer_technology == ptSLA) { for (ModelObject* model_object : p->model.objects) { @@ -18485,7 +20549,7 @@ bool Plater::set_printer_technology(PrinterTechnology printer_technology) } p->label_btn_export = printer_technology == ptFFF ? L("Export G-code") : L("Export"); - p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer"); + p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer"); if (wxGetApp().mainframe != nullptr) wxGetApp().mainframe->update_menubar(); @@ -18506,7 +20570,8 @@ void Plater::clear_before_change_mesh(int obj_idx) // may be different and they would make no sense. bool paint_removed = false; for (ModelVolume* mv : mo->volumes) { - paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mmu_segmentation_facets.empty() || !mv->fuzzy_skin_facets.empty(); + paint_removed |= !mv->supported_facets.empty() || !mv->seam_facets.empty() || !mv->mmu_segmentation_facets.empty() || + !mv->fuzzy_skin_facets.empty(); mv->supported_facets.reset(); mv->seam_facets.reset(); mv->mmu_segmentation_facets.reset(); @@ -18514,10 +20579,9 @@ void Plater::clear_before_change_mesh(int obj_idx) } if (paint_removed) { // snapshot_time is captured by copy so the lambda knows where to undo/redo to. - get_notification_manager()->push_notification( - NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::PrintInfoNotificationLevel, - _u8L("Custom supports and color painting were removed before repairing.")); + get_notification_manager()->push_notification(NotificationType::CustomSupportsAndSeamRemovedAfterRepair, + NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + _u8L("Custom supports and color painting were removed before repairing.")); } } @@ -18530,7 +20594,8 @@ void Plater::changed_mesh(int obj_idx) p->schedule_background_process(); } -void Plater::changed_object(ModelObject &object){ +void Plater::changed_object(ModelObject& object) +{ assert(object.get_model() == &p->model); // is object from same model? object.invalidate_bounding_box(); @@ -18546,7 +20611,7 @@ void Plater::changed_object(ModelObject &object){ // update print p->schedule_background_process(); - + // Check outside bed get_current_canvas3D()->requires_check_outside_state(); } @@ -18555,7 +20620,7 @@ void Plater::changed_object(int obj_idx) { if (obj_idx < 0) return; - ModelObject *object = p->model.objects[obj_idx]; + ModelObject* object = p->model.objects[obj_idx]; if (object == nullptr) return; changed_object(*object); @@ -18577,8 +20642,7 @@ void Plater::changed_objects(const std::vector& object_idxs) // Update the SLAPrint from the current Model, so that the reload_scene() // pulls the correct data, update the 3D scene. this->p->update_restart_background_process(true, false); - } - else { + } else { p->view3D->reload_scene(false); p->view3D->get_canvas3d()->update_instance_printable_state_for_objects(object_idxs); } @@ -18587,7 +20651,7 @@ void Plater::changed_objects(const std::vector& object_idxs) this->p->schedule_background_process(); } -void Plater::schedule_background_process(bool schedule/* = true*/) +void Plater::schedule_background_process(bool schedule /* = true*/) { if (schedule) this->p->schedule_background_process(); @@ -18595,10 +20659,7 @@ void Plater::schedule_background_process(bool schedule/* = true*/) this->p->suppressed_backround_processing_update = false; } -bool Plater::is_background_process_update_scheduled() const -{ - return this->p->background_process_timer.IsRunning(); -} +bool Plater::is_background_process_update_scheduled() const { return this->p->background_process_timer.IsRunning(); } void Plater::suppress_background_process(const bool stop_background_process) { @@ -18611,28 +20672,29 @@ void Plater::suppress_background_process(const bool stop_background_process) // Expose the slicing process to the device GUI. BackgroundSlicingProcess& Plater::background_process() { return p->background_process; } -void Plater::center_selection() { p->center_selection(); } -void Plater::drop_selection() { p->drop_selection(); } -void Plater::mirror(Axis axis) { p->mirror(axis); } -void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); } -void Plater::split_volume() { p->split_volume(); } +void Plater::center_selection() { p->center_selection(); } +void Plater::drop_selection() { p->drop_selection(); } +void Plater::mirror(Axis axis) { p->mirror(axis); } +void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); } +void Plater::split_volume() { p->split_volume(); } void Plater::optimize_rotation() { - auto &w = get_ui_job_worker(); + auto& w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Optimize Rotation")); replace_job(w, std::make_unique()); } } -void Plater::update_menus() { p->menus.update(); } +void Plater::update_menus() { p->menus.update(); } -wxString Plater::get_selected_printer_name_in_combox() { - PresetBundle * preset_bundle = wxGetApp().preset_bundle; - std::string printer_model = preset_bundle->printers.get_selected_preset().config.option("printer_model")->value; +wxString Plater::get_selected_printer_name_in_combox() +{ + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + std::string printer_model = preset_bundle->printers.get_selected_preset().config.option("printer_model")->value; return printer_model; } -void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWarningType type, const wxString &title) +void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWarningType type, const wxString& title) { printer_name.Replace("Bambu Lab", "", false); wxString content; @@ -18646,14 +20708,16 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar _L("OrcaSlicer can't connect to %s. Please check if the printer is powered on and connected to the network."), printer_name); } } else if (type == PrinterWarningType::INCONSISTENT) { - content = wxString::Format(_L("The currently connected printer on the device page is not %s. Please switch to %s before syncing."), printer_name, printer_name); + content = wxString::Format(_L("The currently connected printer on the device page is not %s. Please switch to %s before syncing."), + printer_name, printer_name); } else if (type == PrinterWarningType::UNINSTALL_FILAMENT) { content = _L("There are no filaments on the printer. Please load the filaments on the printer first."); } else if (type == PrinterWarningType::EMPTY_FILAMENT) { - content = _L("The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set the filament type."); + content = _L("The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set " + "the filament type."); } MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page")); - auto result = dlg.ShowModal(); + auto result = dlg.ShowModal(); if (result == wxFORWARD) { wxGetApp().mainframe->select_tab(TAB_ID_MONITOR); } @@ -18664,13 +20728,13 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) if (!wxGetApp().getDeviceManager()) { return false; } - MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); + MachineObject* obj = wxGetApp().getDeviceManager()->get_selected_machine(); if (obj == nullptr) { return false; } if (!check_printer_initialized(obj, true, popup_warning)) return false; - Preset * machine_preset = get_printer_preset(obj); + Preset* machine_preset = get_printer_preset(obj); if (!machine_preset) return false; @@ -18684,7 +20748,7 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) return true; } // BBS -//void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } +// void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } void Plater::fill_color(int extruder_id) { @@ -18693,7 +20757,7 @@ void Plater::fill_color(int extruder_id) } } -//BBS +// BBS void Plater::cut_selection_to_clipboard() { Plater::TakeSnapshot snapshot(this, "Cut Selected Objects"); @@ -18725,7 +20789,7 @@ void Plater::paste_from_clipboard() p->view3D->get_canvas3d()->get_selection().paste_from_clipboard(); } -//BBS: add clone +// BBS: add clone void Plater::clone_selection() { if (is_selection_empty()) @@ -18736,13 +20800,13 @@ void Plater::clone_selection() std::vector Plater::get_empty_cells(const Vec2f step) { - PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); BoundingBoxf3 build_volume = plate->get_build_volume(true); Vec2d vmin(build_volume.min.x(), build_volume.min.y()), vmax(build_volume.max.x(), build_volume.max.y()); BoundingBoxf bbox(vmin, vmax); std::vector cells; - auto min_x = step(0)/2;// start_point.x() - step(0) * int((start_point.x() - bbox.min.x()) / step(0)); - auto min_y = step(1)/2;// start_point.y() - step(1) * int((start_point.y() - bbox.min.y()) / step(1)); + auto min_x = step(0) / 2; // start_point.x() - step(0) * int((start_point.x() - bbox.min.x()) / step(0)); + auto min_y = step(1) / 2; // start_point.y() - step(1) * int((start_point.y() - bbox.min.y()) / step(1)); auto& exclude_box3s = plate->get_exclude_areas(); std::vector exclude_boxs; for (auto& box : exclude_box3s) { @@ -18759,14 +20823,14 @@ std::vector Plater::get_empty_cells(const Vec2f step) break; } } - if(in_exclude) + if (in_exclude) continue; cells.emplace_back(x, y); } return cells; } -void Plater::search(bool plater_is_active, Preset::Type type, wxWindow *tag, TextInput *etag, wxWindow *stag) +void Plater::search(bool plater_is_active, Preset::Type type, wxWindow* tag, TextInput* etag, wxWindow* stag) { if (plater_is_active) { if (is_preview_shown()) @@ -18777,13 +20841,12 @@ void Plater::search(bool plater_is_active, Preset::Type type, wxWindow *tag, Tex wxKeyEvent evt; #ifdef __APPLE__ evt.m_keyCode = 'f'; -#else /* __APPLE__ */ +#else /* __APPLE__ */ evt.m_keyCode = WXK_CONTROL_F; #endif /* __APPLE__ */ evt.SetControlDown(true); canvas3D()->on_char(evt); - } - else + } else p->sidebar->get_searcher().show_dialog(type, tag, etag, stag); } @@ -18806,7 +20869,8 @@ void Plater::sys_color_changed() p->preview->sys_color_changed(); p->sidebar->sys_color_changed(); p->menus.sys_color_changed(); - if (p->m_select_machine_dlg) p->m_select_machine_dlg->sys_color_changed(); + if (p->m_select_machine_dlg) + p->m_select_machine_dlg->sys_color_changed(); Layout(); GetParent()->Layout(); @@ -18825,60 +20889,48 @@ void Plater::enable_view_toolbar(bool enable) } #endif -bool Plater::init_collapse_toolbar() -{ - return p->init_collapse_toolbar(); -} +bool Plater::init_collapse_toolbar() { return p->init_collapse_toolbar(); } -const Camera& Plater::get_camera() const -{ - return p->camera; -} +const Camera& Plater::get_camera() const { return p->camera; } -Camera& Plater::get_camera() -{ - return p->camera; -} +Camera& Plater::get_camera() { return p->camera; } -//BBS: partplate list related functions -PartPlateList& Plater::get_partplate_list() -{ - return p->partplate_list; -} +// BBS: partplate list related functions +PartPlateList& Plater::get_partplate_list() { return p->partplate_list; } void Plater::apply_background_progress() { - PartPlate* part_plate = p->partplate_list.get_curr_plate(); - int plate_index = p->partplate_list.get_curr_plate_index(); - bool result_valid = part_plate->is_slice_result_valid(); + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + int plate_index = p->partplate_list.get_curr_plate_index(); + bool result_valid = part_plate->is_slice_result_valid(); const auto& preset_bundle = wxGetApp().preset_bundle; - //always apply the current plate's print + // always apply the current plate's print Print::ApplyStatus invalidated; if (preset_bundle->get_printer_extruder_count() > 1) { - std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = part_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); - } - else + } else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % plate_index % invalidated % result_valid; - if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) - { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % + plate_index % invalidated % result_valid; + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) { part_plate->update_slice_result_valid_state(false); - //p->ready_to_slice = true; + // p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); } } -//BBS: select Plate +// BBS: select Plate int Plater::select_plate(int plate_index, bool need_slice) { int ret; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, need_slice %3% ")%__LINE__ %plate_index %need_slice; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, need_slice %3% ") % __LINE__ % plate_index % need_slice; take_snapshot("select partplate!"); ret = p->partplate_list.select_plate(plate_index); if (!ret) { @@ -18887,124 +20939,106 @@ int Plater::select_plate(int plate_index, bool need_slice) } const auto& preset_bundle = wxGetApp().preset_bundle; - if ((!ret) && (p->background_process.can_switch_print())) - { - //select successfully + if ((!ret) && (p->background_process.can_switch_print())) { + // select successfully p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->update_print_volume_state(); - PartPlate* part_plate = p->partplate_list.get_curr_plate(); - bool result_valid = part_plate->is_slice_result_valid(); - PrintBase* print = nullptr; + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + bool result_valid = part_plate->is_slice_result_valid(); + PrintBase* print = nullptr; GCodeResult* gcode_result = nullptr; Print::ApplyStatus invalidated; part_plate->get_print(&print, &gcode_result, NULL); - //always apply the current plate's print + // always apply the current plate's print if (preset_bundle->get_printer_extruder_count() > 1) { - std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = part_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); - } - else + } else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); bool model_fits, validate_err; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ")%__LINE__ %plate_index %invalidated %result_valid; - if (result_valid) - { - if (is_preview_shown()) - { - if (need_slice) { //from preview's thumbnail - if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) || (gcode_result->moves.empty())){ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % + plate_index % invalidated % result_valid; + if (result_valid) { + if (is_preview_shown()) { + if (need_slice) { // from preview's thumbnail + if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) || (gcode_result->moves.empty())) { if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) part_plate->update_slice_result_valid_state(false); p->process_completed_with_error = -1; - p->m_slice_all = false; + p->m_slice_all = false; reset_gcode_toolpaths(); reslice(); - } - else { + } else { validate_current_plate(model_fits, validate_err); - //just refresh_print + // just refresh_print reload_print(); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, true); } + } else { // from multiple slice's next + // do nothing } - else {// from multiple slice's next - //do nothing - } - } - else - { + } else { validate_current_plate(model_fits, validate_err); - if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) - { + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) { part_plate->update_slice_result_valid_state(false); // BBS - //p->show_action_buttons(true); - //p->ready_to_slice = true; + // p->show_action_buttons(true); + // p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } - else - { + } else { // BBS - //p->show_action_buttons(false); - //p->ready_to_slice = false; + // p->show_action_buttons(false); + // p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); reload_print(); } } - } - else - { - //check inside status - //model_fits = p->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; - //bool validate_err = false; + } else { + // check inside status + // model_fits = p->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + // bool validate_err = false; validate_current_plate(model_fits, validate_err); if (model_fits && !validate_err) { p->process_completed_with_error = -1; - } - else { + } else { p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); } - if (is_preview_shown()) - { - if (need_slice) - { - //p->process_completed_with_error = -1; + if (is_preview_shown()) { + if (need_slice) { + // p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); if (model_fits && !validate_err) { - if (!check_ams_status(false)){ + if (!check_ams_status(false)) { return ret; } reslice(); - } - else { + } else { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); - //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary - //then the print toolpath will be shown, so we should not refresh print here, only onload shell - //refresh_print(); + // sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary + // then the print toolpath will be shown, so we should not refresh print here, only onload shell + // refresh_print(); p->update_fff_scene_only_shells(); } - } - else { - //p->ready_to_slice = false; + } else { + // p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); reload_print(); } - } - else - { - //validate_current_plate(model_fits, validate_err); - //check inside status + } else { + // validate_current_plate(model_fits, validate_err); + // check inside status /*if (model_fits && !validate_err){ p->process_completed_with_error = -1; } @@ -19013,16 +21047,13 @@ int Plater::select_plate(int plate_index, bool need_slice) }*/ // BBS: don't show action buttons - //p->show_action_buttons(true); - //p->ready_to_slice = true; - if (model_fits && part_plate->has_printable_instances()) - { - //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); + // p->show_action_buttons(true); + // p->ready_to_slice = true; + if (model_fits && part_plate->has_printable_instances()) { + // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } - else - { - //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + } else { + // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); } } @@ -19032,7 +21063,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%") % __LINE__ % plate_index % ret; return ret; } @@ -19043,13 +21074,12 @@ int Plater::select_sliced_plate(int plate_index, bool skip_zoom) Freeze(); ret = select_plate(plate_index, true); - if (ret) - { + if (ret) { BOOST_LOG_TRIVIAL(error) << "select_plate error for plate_idx=" << plate_index; Thaw(); return -1; } - if(skip_zoom) + if (skip_zoom) p->partplate_list.select_plate_view(); Thaw(); @@ -19065,11 +21095,13 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) { ObjectFilamentResults object_results; ModelInstanceEPrintVolumeState state = p->view3D->get_canvas3d()->check_volumes_outside_state(&object_results); - model_fits = (state != ModelInstancePVS_Partly_Outside); + model_fits = (state != ModelInstancePVS_Partly_Outside); - PartPlate *cur_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); - if (model_fits) { // TPU check - bool tpu_valid = cur_plate->check_tpu_printable_status(wxGetApp().preset_bundle->full_config(), wxGetApp().preset_bundle->get_used_tpu_filaments(cur_plate->get_extruders(true))); + PartPlate* cur_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + if (model_fits) { // TPU check + bool tpu_valid = cur_plate->check_tpu_printable_status(wxGetApp().preset_bundle->full_config(), + wxGetApp().preset_bundle->get_used_tpu_filaments( + cur_plate->get_extruders(true))); model_fits &= tpu_valid; } @@ -19079,19 +21111,23 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) model_fits &= filament_printable; } - model_fits = model_fits && object_results.filaments.empty(); + model_fits = model_fits && object_results.filaments.empty(); validate_error = false; if (p->printer_technology == ptFFF) { - //std::string plater_text = _u8L("An object is laid over the boundary of plate or exceeds the height limit.\n" - // "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.");; + // std::string plater_text = _u8L("An object is laid over the boundary of plate or exceeds the height limit.\n" + // "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the + // build volume.");; std::vector warnings; Polygons polygons; std::vector> height_polygons; - p->background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); + p->background_process.fff_print()->set_check_multi_filaments_compatibility( + wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); StringObjectException err = p->background_process.validate(&warnings, &polygons, &height_polygons); // update string by type post_process_string_object_exception(err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%, model_fits %3%")%err.string%warnings.size() %model_fits; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": validate err=%1%, warnings=%2%, model_fits %3%") % err.string % warnings.size() % + model_fits; if (err.string.empty()) { p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -19103,14 +21139,13 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) p->view3D->get_canvas3d()->reset_sequential_print_clearance(); p->view3D->get_canvas3d()->set_as_dirty(); p->view3D->get_canvas3d()->request_extra_frame(); - } - else { + } else { // The print is not valid. p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); // Show error as notification. p->notification_manager->push_validate_error_notification(err); p->process_validation_warnings(warnings); - //model_fits = false; + // model_fits = false; validate_error = true; p->view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); p->view3D->get_canvas3d()->set_sequential_print_clearance_render_fill(true); @@ -19120,25 +21155,25 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) std::string clashed_text = get_object_clashed_text(); if (state == ModelInstancePVS_Partly_Outside) { p->notification_manager->push_plater_error_notification(clashed_text); - } - else { + } else { p->notification_manager->close_plater_error_notification(clashed_text); } - std::string left_unprintable_text = get_left_extruder_unprintable_text(), right_unprintable_text = get_right_extruder_unprintable_text(); - if (!left_unprintable_text.empty()) - { - p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError, left_unprintable_text); - } - else { - p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError); + std::string left_unprintable_text = get_left_extruder_unprintable_text(), + right_unprintable_text = get_right_extruder_unprintable_text(); + if (!left_unprintable_text.empty()) { + p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError, + left_unprintable_text); + } else { + p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice( + NotificationType::LeftExtruderUnprintableError); } - if (!right_unprintable_text.empty()) - { - p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError,right_unprintable_text); - } - else { - p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError); + if (!right_unprintable_text.empty()) { + p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError, + right_unprintable_text); + } else { + p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice( + NotificationType::RightExtruderUnprintableError); } /*if (state == ModelInstancePVS_Limited) { @@ -19196,16 +21231,18 @@ bool Plater::refresh_missing_plugin_block(bool* block_toggled) std::vector refs = missing_refs(missing); std::sort(refs.begin(), refs.end()); std::string sig; - for (const std::string& r : refs) { sig += r; sig += '\n'; } + for (const std::string& r : refs) { + sig += r; + sig += '\n'; + } return sig; }; // Show/refresh the non-closable notification for one missing set. Only (re)create it when the // set changes; pushing every validate would close+recreate it (flicker, reset hover) since // validate runs on many triggers. shown_sig also gates plugin-load re-validation. - const auto update = [&](NotificationType type, const std::vector& missing, - std::string* shown_sig, const std::string& header, - const std::string& resolve_label, + const auto update = [&](NotificationType type, const std::vector& missing, std::string* shown_sig, + const std::string& header, const std::string& resolve_label, std::function resolve_action) { if (missing.empty()) { if (!shown_sig->empty()) { @@ -19220,46 +21257,49 @@ bool Plater::refresh_missing_plugin_block(bool* block_toggled) for (const auto& m : missing) body.emplace_back(JumpTo{m.ref.capability_name, m.opt, m.opt_type}); - p->notification_manager->push_plugin_missing_notification( - type, header, resolve_label, std::move(body), std::move(resolve_action)); + p->notification_manager->push_plugin_missing_notification(type, header, resolve_label, std::move(body), + std::move(resolve_action)); *shown_sig = sig; } }; - const std::vector missing_cloud = get_missing_cloud_plugins(); - const std::vector missing_local = get_missing_local_plugins(); + const std::vector missing_cloud = get_missing_cloud_plugins(); + const std::vector missing_local = get_missing_local_plugins(); const std::vector missing_cloud_refs = missing_refs(missing_cloud); const std::vector missing_local_refs = missing_refs(missing_local); - update(NotificationType::OrcaCloudPluginMissingError, missing_cloud, - &p->m_cloud_missing_shown_sig, - _u8L("OrcaCloud plugins required by the current preset are not installed:"), - _u8L("Install Plugins"), - [this, missing_cloud_refs](wxEvtHandler*) { install_missing_cloud_plugins(missing_cloud_refs); return false; }); + update(NotificationType::OrcaCloudPluginMissingError, missing_cloud, &p->m_cloud_missing_shown_sig, + _u8L("OrcaCloud plugins required by the current preset are not installed:"), _u8L("Install Plugins"), + [this, missing_cloud_refs](wxEvtHandler*) { + install_missing_cloud_plugins(missing_cloud_refs); + return false; + }); // "Find on OrcaCloud" is only a suggestion: it opens the browser but cannot resolve the missing // plugin in-session, so it never closes the notification or unblocks slicing. The user resolves a // local plugin by installing it or by changing the setting that needs it. - update(NotificationType::OrcaLocalPluginMissingError, missing_local, - &p->m_local_missing_shown_sig, - _u8L("Local plugins required by the current preset are missing:"), - _u8L("Find on OrcaCloud"), - [missing_local_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(missing_local_refs); return false; }); + update(NotificationType::OrcaLocalPluginMissingError, missing_local, &p->m_local_missing_shown_sig, + _u8L("Local plugins required by the current preset are missing:"), _u8L("Find on OrcaCloud"), + [missing_local_refs](wxEvtHandler*) { + open_missing_plugins_on_cloud(missing_local_refs); + return false; + }); - const std::vector inactive = get_inactive_plugins(); - const std::vector broken = get_broken_plugins(); - const std::vector inactive_refs = missing_refs(inactive); - const std::vector broken_refs = missing_refs(broken); + const std::vector inactive = get_inactive_plugins(); + const std::vector broken = get_broken_plugins(); + const std::vector inactive_refs = missing_refs(inactive); + const std::vector broken_refs = missing_refs(broken); - update(NotificationType::OrcaPluginInactiveError, inactive, - &p->m_inactive_shown_sig, - _u8L("Plugins required by the current preset are not activated:"), - _u8L("Activate Now"), - [this, inactive_refs](wxEvtHandler*) { enable_inactive_plugins(inactive_refs); return false; }); - update(NotificationType::OrcaPluginCapabilityUnavailableError, broken, - &p->m_broken_shown_sig, - _u8L("The installed plugin does not provide the required capability — it may be outdated:"), - _u8L("Find on OrcaCloud"), - [broken_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(broken_refs); return false; }); + update(NotificationType::OrcaPluginInactiveError, inactive, &p->m_inactive_shown_sig, + _u8L("Plugins required by the current preset are not activated:"), _u8L("Activate Now"), [this, inactive_refs](wxEvtHandler*) { + enable_inactive_plugins(inactive_refs); + return false; + }); + update(NotificationType::OrcaPluginCapabilityUnavailableError, broken, &p->m_broken_shown_sig, + _u8L("The installed plugin does not provide the required capability — it may be outdated:"), _u8L("Find on OrcaCloud"), + [broken_refs](wxEvtHandler*) { + open_missing_plugins_on_cloud(broken_refs); + return false; + }); const bool blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins(); if (block_toggled) @@ -19271,8 +21311,8 @@ void Plater::revalidate_current_plate_if_plugins_missing() { // Only do work while a missing-plugin notification is up, so the plugin-load hook does not // trigger a full validation for every plugin that loads during normal startup/use. - if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() && - p->m_inactive_shown_sig.empty() && p->m_broken_shown_sig.empty()) + if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() && p->m_inactive_shown_sig.empty() && + p->m_broken_shown_sig.empty()) return; bool model_fits = true, validate_error = false; validate_current_plate(model_fits, validate_error); @@ -19289,15 +21329,14 @@ void Plater::install_missing_cloud_plugins(const std::vector& cloud std::atomic cancel{false}; std::atomic finished{false}; std::atomic torn_down{false}; - std::mutex mtx; - std::string message; + std::mutex mtx; + std::string message; }; auto state = std::make_shared(); state->message = _u8L("Preparing to install plugins..."); wxWindow* parent = wxGetApp().mainframe; - auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100, - parent, wxPD_APP_MODAL | wxPD_CAN_ABORT); + auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100, parent, wxPD_APP_MODAL | wxPD_CAN_ABORT); dialog->Pulse(); // UI-thread timer: animate the pulse, observe the Cancel button, and tear down when the worker @@ -19355,7 +21394,8 @@ bool Plater::plugins_block_slicing() const return has_missing_plugins() || has_inactive_plugins() || has_broken_plugins(); } -void Plater::open_platesettings_dialog(wxCommandEvent& evt) { +void Plater::open_platesettings_dialog(wxCommandEvent& evt) +{ int plate_index = evt.GetInt(); PlateSettingsDialog dlg(this, _L("Plate Settings"), evt.GetString() == "only_layer_sequence"); PartPlate* curr_plate = p->partplate_list.get_curr_plate(); @@ -19364,8 +21404,7 @@ void Plater::open_platesettings_dialog(wxCommandEvent& evt) { auto curr_print_seq = curr_plate->get_print_seq(); if (curr_print_seq != PrintSequence::ByDefault) { dlg.sync_print_seq(int(curr_print_seq) + 1); - } - else + } else dlg.sync_print_seq(0); auto first_layer_print_seq = curr_plate->get_first_layer_print_sequence(); @@ -19385,8 +21424,8 @@ void Plater::open_platesettings_dialog(wxCommandEvent& evt) { dlg.Bind(EVT_SET_BED_TYPE_CONFIRM, [this, plate_index, &dlg](wxCommandEvent& e) { PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - BedType old_bed_type = curr_plate->get_bed_type(); - auto bt_sel = dlg.get_bed_type_choice(); + BedType old_bed_type = curr_plate->get_bed_type(); + auto bt_sel = dlg.get_bed_type_choice(); if (old_bed_type != bt_sel) { curr_plate->set_bed_type(bt_sel); update_project_dirty_from_presets(); @@ -19413,63 +21452,54 @@ void Plater::open_platesettings_dialog(wxCommandEvent& evt) { int spiral_sel = dlg.get_spiral_mode_choice(); if (spiral_sel == 1) { curr_plate->set_spiral_vase_mode(true, false); - } - else if (spiral_sel == 2) { + } else if (spiral_sel == 2) { curr_plate->set_spiral_vase_mode(false, false); - } - else { + } else { curr_plate->set_spiral_vase_mode(false, true); } update_project_dirty_from_presets(); set_plater_dirty(true); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("select print sequence %1% for plate %2% at plate side") % ps_sel % plate_index; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format("select print sequence %1% for plate %2% at plate side") % ps_sel % plate_index; auto plate_config = *(curr_plate->config()); wxGetApp().plater()->config_change_notification(plate_config, std::string("print_sequence")); update(); wxGetApp().obj_list()->update_selections(); - }); + }); dlg.set_plate_name(from_u8(curr_plate->get_plate_name())); dlg.ShowModal(); curr_plate->set_plate_name(dlg.get_plate_name().ToUTF8().data()); } -void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) +void Plater::open_filament_map_setting_dialog(wxCommandEvent& evt) { PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - int value = evt.GetInt(); //1 means from gcode view - bool need_slice = value ==1; // If from gcode view, should slice + int value = evt.GetInt(); // 1 means from gcode view + bool need_slice = value == 1; // If from gcode view, should slice const auto& project_config = wxGetApp().preset_bundle->project_config; - auto filament_colors = config()->option("filament_colour")->values; - auto filament_types = config()->option("filament_type")->values; + auto filament_colors = config()->option("filament_colour")->values; + auto filament_types = config()->option("filament_type")->values; - auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config); - auto plate_filament_map_mode = curr_plate->get_filament_map_mode(); + auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config); + auto plate_filament_map_mode = curr_plate->get_filament_map_mode(); auto plate_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config); - if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config + if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config plate_filament_maps.resize(filament_colors.size(), 1); if (plate_filament_volume_maps.size() != filament_colors.size()) plate_filament_volume_maps.resize(filament_colors.size(), 0); - FilamentMapDialog filament_dlg(this, - filament_colors, - filament_types, - plate_filament_maps, - plate_filament_volume_maps, - curr_plate->get_extruders(true), - plate_filament_map_mode, - this->get_machine_sync_status(), - false - ); + FilamentMapDialog filament_dlg(this, filament_colors, filament_types, plate_filament_maps, plate_filament_volume_maps, + curr_plate->get_extruders(true), plate_filament_map_mode, this->get_machine_sync_status(), false); if (filament_dlg.ShowModal() == wxID_OK) { std::vector new_filament_maps = filament_dlg.get_filament_maps(); std::vector old_filament_maps = curr_plate->get_real_filament_maps(project_config); - FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode(); - FilamentMapMode new_map_mode = filament_dlg.get_mode(); + FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode(); + FilamentMapMode new_map_mode = filament_dlg.get_mode(); std::vector new_filament_volume_maps = filament_dlg.get_filament_volume_maps(); std::vector old_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config); @@ -19478,20 +21508,18 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) curr_plate->set_filament_map_mode(new_map_mode); } - if (new_map_mode == fmmManual){ + if (new_map_mode == fmmManual) { curr_plate->set_filament_maps(new_filament_maps); curr_plate->set_filament_volume_maps(new_filament_volume_maps); } - bool need_invalidate = (old_map_mode != new_map_mode || - old_filament_maps != new_filament_maps || + bool need_invalidate = (old_map_mode != new_map_mode || old_filament_maps != new_filament_maps || old_filament_volume_maps != new_filament_volume_maps); if (need_invalidate) { if (need_slice) { wxPostEvent(this, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - } - else { + } else { curr_plate->update_slice_result_valid_state(false); set_plater_dirty(true); update(); @@ -19501,8 +21529,7 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) return; } - -//BBS: select Plate by hover_id +// BBS: select Plate by hover_id int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModidyPlateName) { int ret; @@ -19511,145 +21538,121 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi plate_index = hover_id / PartPlate::GRABBER_COUNT; action = isModidyPlateName ? PartPlate::PLATE_NAME_HOVER_ID : hover_id % PartPlate::GRABBER_COUNT; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, hover_id %1%, plate_index %2%, action %3%")%hover_id % plate_index %action; - if (action == 0) - { - //select plate + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": enter, hover_id %1%, plate_index %2%, action %3%") % hover_id % plate_index % action; + if (action == 0) { + // select plate ret = p->partplate_list.select_plate(plate_index); if (!ret) { SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); } - if ((!ret)&&(p->background_process.can_switch_print())) - { - //select successfully + if ((!ret) && (p->background_process.can_switch_print())) { + // select successfully p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->update_print_volume_state(); - PartPlate* part_plate = p->partplate_list.get_curr_plate(); - bool result_valid = part_plate->is_slice_result_valid(); - PrintBase* print = nullptr; + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + bool result_valid = part_plate->is_slice_result_valid(); + PrintBase* print = nullptr; GCodeResult* gcode_result = nullptr; Print::ApplyStatus invalidated; const auto& preset_bundle = wxGetApp().preset_bundle; part_plate->get_print(&print, &gcode_result, NULL); - //always apply the current plate's print + // always apply the current plate's print if (preset_bundle->get_printer_extruder_count() > 1) { - std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = part_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); - } - else + } else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); bool model_fits, validate_err; validate_current_plate(model_fits, validate_err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: after apply, invalidated= %2%, previous result_valid %3% ")%__LINE__ % invalidated %result_valid; - if (result_valid) - { - if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) - { - //bool model_fits, validate_err; - //validate_current_plate(model_fits, validate_err); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(" %1%: after apply, invalidated= %2%, previous result_valid %3% ") % __LINE__ % + invalidated % result_valid; + if (result_valid) { + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) { + // bool model_fits, validate_err; + // validate_current_plate(model_fits, validate_err); part_plate->update_slice_result_valid_state(false); // BBS - //p->show_action_buttons(true); - //p->ready_to_slice = true; + // p->show_action_buttons(true); + // p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } - else - { + } else { // BBS - //p->show_action_buttons(false); - //validate_current_plate(model_fits, validate_err); - //p->ready_to_slice = false; + // p->show_action_buttons(false); + // validate_current_plate(model_fits, validate_err); + // p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); reload_print(); } - } - else - { - //check inside status - if (model_fits && !validate_err){ + } else { + // check inside status + if (model_fits && !validate_err) { p->process_completed_with_error = -1; - } - else { + } else { p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); } // BBS: don't show action buttons - //p->show_action_buttons(true); - //p->ready_to_slice = true; - if (model_fits && part_plate->has_printable_instances()) - { - //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); + // p->show_action_buttons(true); + // p->ready_to_slice = true; + if (model_fits && part_plate->has_printable_instances()) { + // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": will set can_slice to true"); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } - else - { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": will set can_slice to false, has_printable_instances %1%")%part_plate->has_printable_instances(); - //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": will set can_slice to false, has_printable_instances %1%") % + part_plate->has_printable_instances(); + // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); } } } - } - else if ((action == 1)&&(!right_click)) - { - //delete plate + } else if ((action == 1) && (!right_click)) { + // delete plate ret = delete_plate(plate_index); - } - else if ((action == 2)&&(!right_click)) - { - //arrange the plate - //take_snapshot("select_orient partplate"); + } else if ((action == 2) && (!right_click)) { + // arrange the plate + // take_snapshot("select_orient partplate"); ret = select_plate(plate_index); - if (!ret) - { + if (!ret) { set_prepare_state(Job::PREPARE_STATE_MENU); orient(); - } - else - { + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } - else if ((action == 3)&&(!right_click)) - { - //arrange the plate - //take_snapshot("select_arrange partplate"); + } else if ((action == 3) && (!right_click)) { + // arrange the plate + // take_snapshot("select_arrange partplate"); ret = select_plate(plate_index); - if (!ret) - { + if (!ret) { if (last_arrange_job_is_finished()) { set_prepare_state(Job::PREPARE_STATE_MENU); arrange(); } - } - else - { + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } - else if ((action == 4)&&(!right_click)) - { - //lock the plate + } else if ((action == 4) && (!right_click)) { + // lock the plate take_snapshot("lock partplate"); ret = p->partplate_list.lock_plate(plate_index, !p->partplate_list.is_locked(plate_index)); - } - else if ((action == 5)&&(!right_click)) - { + } else if ((action == 5) && (!right_click)) { // set the plate type ret = select_plate(plate_index); if (!ret) { @@ -19663,11 +21666,10 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } - else if ((action == PartPlate::PLATE_FILAMENT_MAP_ID) && (!right_click)) { + } else if ((action == PartPlate::PLATE_FILAMENT_MAP_ID) && (!right_click)) { ret = select_plate(plate_index); if (!ret) { - PartPlate * curr_plate = p->partplate_list.get_curr_plate(); + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); wxCommandEvent evt(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG); evt.SetInt(0); // 0 means not from gcodeviewer evt.SetEventObject(this); @@ -19676,18 +21678,17 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } - else if ((action == 6) && (!right_click)) { + } else if ((action == 6) && (!right_click)) { // set the plate type ret = select_plate(plate_index); if (!ret) { PlateNameEditDialog dlg(this, wxID_ANY, _L("Edit Plate Name")); - PartPlate * curr_plate = p->partplate_list.get_curr_plate(); + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); wxString curr_plate_name = from_u8(curr_plate->get_plate_name()); dlg.set_plate_name(curr_plate_name); - int result=dlg.ShowModal(); + int result = dlg.ShowModal(); if (result == wxID_YES) { wxString dlg_plate_name = dlg.get_plate_name(); curr_plate->set_plate_name(dlg_plate_name.ToUTF8().data()); @@ -19699,7 +21700,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi } else if ((action == 7) && (!right_click)) { // move plate to the front take_snapshot("move plate to the front"); - ret = p->partplate_list.move_plate_to_index(plate_index,0); + ret = p->partplate_list.move_plate_to_index(plate_index, 0); p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->sidebar->obj_list()->reload_all_plates(); @@ -19708,13 +21709,12 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi p->partplate_list.select_plate(0); } - else - { + else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; ret = -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: return %2%")%__LINE__ % ret; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: return %2%") % __LINE__ % ret; return ret; } @@ -19726,12 +21726,12 @@ int Plater::duplicate_plate(int plate_index) ret = p->partplate_list.duplicate_plate(index); - //need to call update + // need to call update update(); return ret; } -//BBS: delete the plate, index= -1 means the current plate +// BBS: delete the plate, index= -1 means the current plate int Plater::delete_plate(int plate_index) { int index = plate_index, ret; @@ -19742,47 +21742,41 @@ int Plater::delete_plate(int plate_index) take_snapshot("delete partplate"); ret = p->partplate_list.delete_plate(index); - //BBS: update the current print to the current plate + // BBS: update the current print to the current plate p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->sidebar->obj_list()->reload_all_plates(); // BBS update default view - //get_camera().select_view("topfront"); - //get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + // get_camera().select_view("topfront"); + // get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; - //need to call update + // need to call update update(); return ret; } -//BBS: set bed positions -void Plater::set_bed_position(Vec2d& pos) -{ - p->bed.set_position(pos); -} +// BBS: set bed positions +void Plater::set_bed_position(Vec2d& pos) { p->bed.set_position(pos); } -//BBS: is the background process slicing currently -bool Plater::is_background_process_slicing() const -{ - return p->m_is_slicing; -} +// BBS: is the background process slicing currently +bool Plater::is_background_process_slicing() const { return p->m_is_slicing; } -//BBS: update slicing context +// BBS: update slicing context void Plater::update_slicing_context_to_current_partplate() { p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); } -//BBS: show object info +// BBS: show object info void Plater::show_object_info() { - NotificationManager *notify_manager = get_notification_manager(); - const Selection& selection = get_selection(); - int selCount = selection.get_volume_idxs().size(); - ModelObjectPtrs objects = model().objects; - int obj_idx = selection.get_object_idx(); + NotificationManager* notify_manager = get_notification_manager(); + const Selection& selection = get_selection(); + int selCount = selection.get_volume_idxs().size(); + ModelObjectPtrs objects = model().objects; + int obj_idx = selection.get_object_idx(); std::string info_text; if (selCount > 1 && !selection.is_single_full_object()) { @@ -19795,25 +21789,22 @@ void Plater::show_object_info() } notify_manager->bbl_show_objectsinfo_notification(info_text, false, !(p->current_panel == p->view3D)); return; - } - else if (objects.empty() || (obj_idx < 0) || (obj_idx >= objects.size()) || - objects[obj_idx]->volumes.empty() ||// hack to avoid crash when deleting the last object on the bed - (selection.is_single_full_object() && objects[obj_idx]->instances.size()> 1) || - !(selection.is_single_full_instance() || selection.is_single_volume())) - { + } else if (objects.empty() || (obj_idx < 0) || (obj_idx >= objects.size()) || + objects[obj_idx]->volumes.empty() || // hack to avoid crash when deleting the last object on the bed + (selection.is_single_full_object() && objects[obj_idx]->instances.size() > 1) || + !(selection.is_single_full_instance() || selection.is_single_volume())) { notify_manager->bbl_close_objectsinfo_notification(); return; } const ModelObject* model_object = objects[obj_idx]; - int inst_idx = selection.get_instance_idx(); - if ((inst_idx < 0) || (inst_idx >= model_object->instances.size())) - { + int inst_idx = selection.get_instance_idx(); + if ((inst_idx < 0) || (inst_idx >= model_object->instances.size())) { notify_manager->bbl_close_objectsinfo_notification(); return; } bool imperial_units = wxGetApp().app_config->get("use_inches") == "1"; - double koef = imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0f; + double koef = imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0f; ModelVolume* vol = nullptr; Transform3d t; @@ -19822,65 +21813,60 @@ void Plater::show_object_info() if (selection.is_single_volume()) { std::vector obj_idxs, vol_idxs; wxGetApp().obj_list()->get_selection_indexes(obj_idxs, vol_idxs); - if (vol_idxs.size() != 1) - { - //corner case when merge/split/remove + if (vol_idxs.size() != 1) { + // corner case when merge/split/remove return; } vol = model_object->volumes[vol_idxs[0]]; - t = model_object->instances[inst_idx]->get_matrix() * vol->get_matrix(); + t = model_object->instances[inst_idx]->get_matrix() * vol->get_matrix(); info_text += (boost::format(_utf8(L("Part name: %1%\n"))) % vol->name).str(); face_count = static_cast(vol->mesh().facets_count()); - size = vol->get_convex_hull().transformed_bounding_box(t).size(); - } - else { - //int obj_idx, vol_idx; - //wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx); - //if (obj_idx < 0) { - // //corner case when merge/split/remove - // return; - //} + size = vol->get_convex_hull().transformed_bounding_box(t).size(); + } else { + // int obj_idx, vol_idx; + // wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx); + // if (obj_idx < 0) { + // //corner case when merge/split/remove + // return; + // } info_text += (boost::format(_utf8(L("Object name: %1%\n"))) % model_object->name).str(); face_count = static_cast(model_object->facets_count()); - size = model_object->instance_convex_hull_bounding_box(inst_idx).size(); + size = model_object->instance_convex_hull_bounding_box(inst_idx).size(); } - //Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); + // Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); if (imperial_units) - info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% in\n"))) %(size(0)*koef) %(size(1)*koef) %(size(2)*koef)).str(); + info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% in\n"))) % (size(0) * koef) % (size(1) * koef) % (size(2) * koef)).str(); else - info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% mm\n"))) %size(0) %size(1) %size(2)).str(); + info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% mm\n"))) % size(0) % size(1) % size(2)).str(); const TriangleMeshStats& stats = vol ? vol->mesh().stats() : model_object->get_object_stl_stats(); - double volume_val = stats.volume; + double volume_val = stats.volume; if (vol) volume_val *= std::fabs(t.matrix().block(0, 0, 3, 3).determinant()); - volume_val = volume_val * pow(koef,3); + volume_val = volume_val * pow(koef, 3); if (imperial_units) - info_text += (boost::format(_utf8(L("Volume: %1% in³\n"))) %volume_val).str(); + info_text += (boost::format(_utf8(L("Volume: %1% in³\n"))) % volume_val).str(); else - info_text += (boost::format(_utf8(L("Volume: %1% mm³\n"))) %volume_val).str(); - info_text += (boost::format(_utf8(L("Triangles: %1%\n"))) %face_count).str(); + info_text += (boost::format(_utf8(L("Volume: %1% mm³\n"))) % volume_val).str(); + info_text += (boost::format(_utf8(L("Triangles: %1%\n"))) % face_count).str(); wxString info_manifold; int non_manifold_edges = 0; - auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); + auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); - if (non_manifold_edges > 0) { - info_manifold += into_u8("\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh.")); - } + if (non_manifold_edges > 0) { + info_manifold += into_u8("\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh.")); + } info_manifold = "" + info_manifold + ""; info_text += into_u8(info_manifold); notify_manager->bbl_show_objectsinfo_notification(info_text, non_manifold_edges > 0, !(p->current_panel == p->view3D)); } -bool Plater::show_publish_dialog(bool show) -{ - return p->show_publish_dlg(show); -} +bool Plater::show_publish_dialog(bool show) { return p->show_publish_dlg(show); } -void Plater::post_process_string_object_exception(StringObjectException &err) +void Plater::post_process_string_object_exception(StringObjectException& err) { PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (err.type == StringExceptionType::STRING_EXCEPT_FILAMENT_NOT_MATCH_BED_TYPE) { @@ -19903,8 +21889,9 @@ void Plater::post_process_string_object_exception(StringObjectException &err) break; } } - err.string = format(_L("Plate %d: %s is not suggested for use printing filament %s (%s). If you still want to do this print job, please set this filament\'s bed temperature to a number that is not zero."), - err.params[0], err.params[1], err.params[2], filament_name); + err.string = format(_L("Plate %d: %s is not suggested for use printing filament %s (%s). If you still want to do this " + "print job, please set this filament\'s bed temperature to a number that is not zero."), + err.params[0], err.params[1], err.params[2], filament_name); err.string += "\n"; } } catch (...) { @@ -19915,10 +21902,8 @@ void Plater::post_process_string_object_exception(StringObjectException &err) return; } -void Plater::update_objects_position_when_select_preset(const std::function &select_prest) -{ - p->update_objects_position_when_select_preset(select_prest); -} +void Plater::update_objects_position_when_select_preset(const std::function& select_prest) +{ p->update_objects_position_when_select_preset(select_prest); } bool Plater::check_ams_status(bool is_slice_all) { @@ -19926,8 +21911,7 @@ bool Plater::check_ams_status(bool is_slice_all) if (!p->check_ams_status_impl(is_slice_all)) { m_check_status = 0; return false; - } - else { + } else { m_check_status = 1; } } @@ -19937,32 +21921,33 @@ bool Plater::check_ams_status(bool is_slice_all) void Plater::update_machine_sync_status() { - DeviceManager *dev_maneger = wxGetApp().getDeviceManager(); + DeviceManager* dev_maneger = wxGetApp().getDeviceManager(); if (!dev_maneger) { GUI::wxGetApp().sidebar().update_sync_status(nullptr); return; } - MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); + MachineObject* obj = wxGetApp().getDeviceManager()->get_selected_machine(); GUI::wxGetApp().sidebar().update_sync_status(obj); } -bool Plater::get_machine_sync_status() -{ - return p->get_machine_sync_status(); -} +bool Plater::get_machine_sync_status() { return p->get_machine_sync_status(); } void Plater::update_filament_volume_map(int extruder_id, int volume_type) { // Hybrid is a per-extruder mix, not a per-filament volume; reset the affected filaments // to Standard so the manual grouping dialog starts from a concrete assignment. - int selected_volume_type = volume_type == static_cast(NozzleVolumeType::nvtHybrid) ? static_cast(NozzleVolumeType::nvtStandard) : volume_type; - auto& partplate_list = get_partplate_list(); + int selected_volume_type = volume_type == static_cast(NozzleVolumeType::nvtHybrid) ? + static_cast(NozzleVolumeType::nvtStandard) : + volume_type; + auto& partplate_list = get_partplate_list(); for (int idx = 0; idx < partplate_list.get_plate_count(); ++idx) { auto plate = partplate_list.get_plate(idx); - if (!plate) continue; + if (!plate) + continue; auto filament_map = plate->get_filament_maps(); auto filament_volume_map = plate->get_filament_volume_maps(); - if (filament_map.empty() || filament_volume_map.empty()) continue; + if (filament_map.empty() || filament_volume_map.empty()) + continue; if (filament_volume_map.size() < filament_map.size()) { filament_volume_map.resize(filament_map.size(), static_cast(NozzleVolumeType::nvtStandard)); } @@ -19982,16 +21967,10 @@ void Plater::init_environment_texture() p->environment_texture.load_from_file(resources_dir() + "/images/Pmetal_001.png", false, GLTexture::SingleThreaded, false); } -unsigned int Plater::get_environment_texture_id() const -{ - return p->environment_texture.get_id(); -} +unsigned int Plater::get_environment_texture_id() const { return p->environment_texture.get_id(); } #endif // ENABLE_ENVIRONMENT_MAP -const BuildVolume& Plater::build_volume() const -{ - return p->bed.build_volume(); -} +const BuildVolume& Plater::build_volume() const { return p->bed.build_volume(); } // BBS #if 0 @@ -20006,42 +21985,24 @@ GLToolbar& Plater::get_view_toolbar() } #endif -const GLToolbar& Plater::get_collapse_toolbar() const -{ - return p->collapse_toolbar; -} +const GLToolbar& Plater::get_collapse_toolbar() const { return p->collapse_toolbar; } -GLToolbar& Plater::get_collapse_toolbar() -{ - return p->collapse_toolbar; -} +GLToolbar& Plater::get_collapse_toolbar() { return p->collapse_toolbar; } -void Plater::update_preview_bottom_toolbar() -{ - p->update_preview_bottom_toolbar(); -} +void Plater::update_preview_bottom_toolbar() { p->update_preview_bottom_toolbar(); } void Plater::reset_gcode_toolpaths() { - //BBS: add some logs + // BBS: add some logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": reset the gcode viewer's toolpaths"); p->reset_gcode_toolpaths(); } -const Mouse3DController& Plater::get_mouse3d_controller() const -{ - return p->mouse3d_controller; -} +const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; } -Mouse3DController& Plater::get_mouse3d_controller() -{ - return p->mouse3d_controller; -} +Mouse3DController& Plater::get_mouse3d_controller() { return p->mouse3d_controller; } -NotificationManager * Plater::get_notification_manager() -{ - return p->notification_manager.get(); -} +NotificationManager* Plater::get_notification_manager() { return p->notification_manager.get(); } DailyTipsWindow* Plater::get_dailytips() const { @@ -20049,20 +22010,11 @@ DailyTipsWindow* Plater::get_dailytips() const return dailytips_win; } -const NotificationManager * Plater::get_notification_manager() const -{ - return p->notification_manager.get(); -} +const NotificationManager* Plater::get_notification_manager() const { return p->notification_manager.get(); } -void Plater::init_notification_manager() -{ - p->init_notification_manager(); -} +void Plater::init_notification_manager() { p->init_notification_manager(); } -void Plater::show_status_message(std::string s) -{ - BOOST_LOG_TRIVIAL(trace) << "show_status_message:" << s; -} +void Plater::show_status_message(std::string s) { BOOST_LOG_TRIVIAL(trace) << "show_status_message:" << s; } bool Plater::can_delete() const { return p->can_delete(); } bool Plater::can_delete_all() const { return p->can_delete_all(); } @@ -20081,9 +22033,10 @@ bool Plater::can_arrange() const { return p->can_arrange(); } bool Plater::can_layers_editing() const { return p->can_layers_editing(); } bool Plater::can_paste_from_clipboard() const { - if (!IsShown() || !p->is_view3D_shown()) return false; + if (!IsShown() || !p->is_view3D_shown()) + return false; - const Selection& selection = p->view3D->get_canvas3d()->get_selection(); + const Selection& selection = p->view3D->get_canvas3d()->get_selection(); const Selection::Clipboard& clipboard = selection.get_clipboard(); if (clipboard.is_empty() && p->sidebar->obj_list()->clipboard_is_empty()) @@ -20102,7 +22055,7 @@ bool Plater::can_paste_from_clipboard() const return true; } -//BBS support cut +// BBS support cut bool Plater::can_cut_to_clipboard() const { if (is_selection_empty()) @@ -20128,7 +22081,7 @@ bool Plater::can_copy_to_clipboard() const bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_undo_snapshot(); } bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_redo_snapshot(); } bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); } -//BBS +// BBS bool Plater::can_fillcolor() const { return p->can_fillcolor(); } bool Plater::has_assmeble_view() const { return p->has_assemble_view(); } bool Plater::can_replace_with_stl() const { return p->can_replace_with_stl(); } @@ -20145,53 +22098,33 @@ void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); } bool Plater::leave_gizmos_stack() { return p->leave_gizmos_stack(); } // BBS: return false if not changed bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); } -void Plater::toggle_render_statistic_dialog() -{ - p->show_render_statistic_dialog = !p->show_render_statistic_dialog; -} +void Plater::toggle_render_statistic_dialog() { p->show_render_statistic_dialog = !p->show_render_statistic_dialog; } -bool Plater::is_render_statistic_dialog_visible() const -{ - return p->show_render_statistic_dialog; -} +bool Plater::is_render_statistic_dialog_visible() const { return p->show_render_statistic_dialog; } -void Plater::toggle_show_wireframe() -{ - p->show_wireframe = !p->show_wireframe; -} +void Plater::toggle_show_wireframe() { p->show_wireframe = !p->show_wireframe; } -bool Plater::is_show_wireframe() const -{ - return p->show_wireframe; -} +bool Plater::is_show_wireframe() const { return p->show_wireframe; } -void Plater::enable_wireframe(bool status) -{ - p->wireframe_enabled = status; -} - -bool Plater::is_wireframe_enabled() const -{ - return p->wireframe_enabled; -} +void Plater::enable_wireframe(bool status) { p->wireframe_enabled = status; } +bool Plater::is_wireframe_enabled() const { return p->wireframe_enabled; } /*Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name) : TakeSnapshot(plater, from_u8(snapshot_name)) {} Plater::TakeSnapshot::TakeSnapshot(Plater* plater, const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type) : TakeSnapshot(plater, from_u8(snapshot_name), snapshot_type) {}*/ - // Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu. -bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos) +bool Plater::PopupMenu(wxMenu* menu, const wxPoint& pos) { // Don't want to wake up and trigger reslicing while tracking the pop-up menu. SuppressBackgroundProcessingUpdate sbpu; // When tracking a pop-up menu, postpone error messages from the slicing result. m_tracking_popup_menu = true; - bool out = wxGetApp().mainframe->PopupMenu(menu, pos); + bool out = wxGetApp().mainframe->PopupMenu(menu, pos); m_tracking_popup_menu = false; - if (! m_tracking_popup_menu_error_message.empty()) { + if (!m_tracking_popup_menu_error_message.empty()) { // Don't know whether the CallAfter is necessary, but it should not hurt. // The menus likely sends out some commands, so we may be safer if the dialog is shown after the menu command is processed. wxString message = std::move(m_tracking_popup_menu_error_message); @@ -20200,27 +22133,19 @@ bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos) } return out; } -void Plater::bring_instance_forward() -{ - p->bring_instance_forward(); -} +void Plater::bring_instance_forward() { p->bring_instance_forward(); } -bool Plater::need_update() const -{ - return p->need_update(); -} +bool Plater::need_update() const { return p->need_update(); } -void Plater::set_need_update(bool need_update) -{ - p->set_need_update(need_update); -} +void Plater::set_need_update(bool need_update) { p->set_need_update(need_update); } // BBS -//BBS: add popup logic for table object +// BBS: add popup logic for table object bool Plater::PopupObjectTable(int object_id, int volume_id, const wxPoint& position) { - if (dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER))->m_extruders_count > 1) { - MessageDialog dlg(this, _L("Currently, the object configuration form cannot be used with a multiple-extruder printer."), _L("Not available"), wxOK | wxICON_WARNING); + if (dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER))->m_extruders_count > 1) { + MessageDialog dlg(this, _L("Currently, the object configuration form cannot be used with a multiple-extruder printer."), + _L("Not available"), wxOK | wxICON_WARNING); dlg.ShowModal(); return false; } @@ -20231,41 +22156,34 @@ bool Plater::PopupObjectTableBySelection() { wxDataViewItem item; int obj_idx, vol_idx; - const wxPoint pos = wxPoint(0, 0); //Fake position + const wxPoint pos = wxPoint(0, 0); // Fake position wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx, item); return p->PopupObjectTable(obj_idx, vol_idx, pos); } -void Plater::update_title_dirty_status() -{ - p->update_title_dirty_status(); -} +void Plater::update_title_dirty_status() { p->update_title_dirty_status(); } - -wxMenu* Plater::plate_menu() { return p->menus.plate_menu(); } -wxMenu* Plater::object_menu() { return p->menus.object_menu(); } -wxMenu* Plater::part_menu() { return p->menus.part_menu(); } -wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } -wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } -wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } -wxMenu* Plater::default_menu() { return p->menus.default_menu(); } -wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } -wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); } -wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); } -wxMenu *Plater::filament_action_menu(int active_filament_menu_id) { return p->menus.filament_action_menu(active_filament_menu_id); } -int Plater::GetPlateIndexByRightMenuInLeftUI() { return p->m_is_RightClickInLeftUI; } -void Plater::SetPlateIndexByRightMenuInLeftUI(int index) { p->m_is_RightClickInLeftUI = index; } -SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() : - m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled()) -{ - wxGetApp().plater()->suppress_background_process(m_was_scheduled); -} +wxMenu* Plater::plate_menu() { return p->menus.plate_menu(); } +wxMenu* Plater::object_menu() { return p->menus.object_menu(); } +wxMenu* Plater::part_menu() { return p->menus.part_menu(); } +wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } +wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } +wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } +wxMenu* Plater::default_menu() { return p->menus.default_menu(); } +wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } +wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); } +wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); } +wxMenu* Plater::filament_action_menu(int active_filament_menu_id) { return p->menus.filament_action_menu(active_filament_menu_id); } +int Plater::GetPlateIndexByRightMenuInLeftUI() { return p->m_is_RightClickInLeftUI; } +void Plater::SetPlateIndexByRightMenuInLeftUI(int index) { p->m_is_RightClickInLeftUI = index; } +SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() + : m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled()) +{ wxGetApp().plater()->suppress_background_process(m_was_scheduled); } SuppressBackgroundProcessingUpdate::~SuppressBackgroundProcessingUpdate() +{ wxGetApp().plater()->schedule_background_process(m_was_scheduled); } +wxString get_view_type_string(Camera::ViewAngleType type) { - wxGetApp().plater()->schedule_background_process(m_was_scheduled); -} -wxString get_view_type_string(Camera::ViewAngleType type) { switch (type) { case Slic3r::GUI::Camera::ViewAngleType::Iso: return _L("isometric"); case Slic3r::GUI::Camera::ViewAngleType::Top_Front: return _L("top_front"); @@ -20278,11 +22196,12 @@ wxString get_view_type_string(Camera::ViewAngleType type) { default: return ""; } } -wxArrayString get_all_camera_view_type() { +wxArrayString get_all_camera_view_type() +{ wxArrayString all_types; - for (size_t i = 0; i < (int)Camera::ViewAngleType::Count_ViewAngleType; i++) { + for (size_t i = 0; i < (int) Camera::ViewAngleType::Count_ViewAngleType; i++) { all_types.Add(get_view_type_string((Camera::ViewAngleType) i)); } return all_types; } -}} // namespace Slic3r::GUI +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 61e6bee15c..2f83da1cb9 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -5,6 +5,7 @@ #include #include +#include #include // BBS #include @@ -86,6 +87,10 @@ using t_optgroups = std::vector >; class Plater; enum class ActionButtonType : int; +// Sentinel filament id meaning "use the slot the sidebar context menu was opened on" +// (Sidebar::priv::m_menu_filament_id) rather than an explicit index. +inline constexpr int kSidebarContextMenuFilamentId = -2; + #define EVT_PUBLISHING_START 1 #define EVT_PUBLISHING_STOP 2 @@ -188,7 +193,7 @@ public: void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default void change_filament(size_t from_id, size_t to_id); // 0 base void edit_filament(); - void add_custom_filament(wxColour new_col); + void add_custom_filament(wxColour new_col, const std::string& preset_name = std::string(), bool skip_preset_validation = false); bool is_new_project_in_gcode3mf(); // BBS void on_bed_type_change(BedType bed_type); @@ -262,6 +267,20 @@ public: std::vector& combos_filament(); void clear_combos_filament_badge(); void udpate_combos_filament_badge(); + + // Mixed-color filament sidebar section + void add_mixed_filament(); + void edit_mixed_filament(size_t idx); + void delete_mixed_filament_at(size_t idx); + void decompose_filament_color(int filament_idx); + void recalc_filament_scroll_sizes(); + void update_mixed_filament_list(); + bool has_broken_mixed_filament() const; + bool has_broken_mixed_filament(const PartPlate* plate) const; + void collect_physical_filament_info(std::vector& color_strs, + std::vector& names, + std::vector& types, + std::vector* config_indices = nullptr); Search::OptionsSearcher& get_searcher(); std::string& get_search_line(); void update_printer_thumbnail(); @@ -313,6 +332,11 @@ public: const SLAPrint& sla_print() const; SLAPrint& sla_print(); + // Helper: returns config indices where filament_is_mixed == true + std::vector mixed_filament_config_indices() const; + // Helper: returns config indices where filament_is_mixed == false + std::vector physical_filament_config_indices() const; + int new_project(bool skip_confirm = false, bool silent = false, const wxString& project_name = wxString()); // BBS: save & backup void load_project(wxString const & filename = "", wxString const & originfile = "-"); @@ -384,8 +408,6 @@ public: // BBS: restore std::vector load_files(const std::vector& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false, bool* published_out = nullptr); - // To be called when providing a list of files to the GUI slic3r on command line. - std::vector load_files(const std::vector& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false); // to be called on drag and drop bool load_files(const wxArrayString& filenames); @@ -571,7 +593,7 @@ public: void on_filament_change(size_t filament_idx); void on_filament_count_change(size_t extruders_count); - void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1); + void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1, const std::vector& is_mixed_before_delete = {}); std::vector get_extruders_colors(); // BBS void on_bed_type_change(BedType bed_type); @@ -586,6 +608,12 @@ public: std::vector get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const; std::vector get_filament_colors_render_info() const; std::vector get_filament_color_render_type() const; + + // Per slot, the colours a gradient mixed filament actually prints, sampled bottom (index 0) + // to top, so the sidebar, the paint gizmo and the extruder icons draw the same fade the + // editor previews rather than a straight blend of two endpoints. A slot that is not a + // gradient mixed filament gets an empty ramp. Cached; recomputed when the config changes. + const std::vector>& get_filament_gradient_ramps() const; std::vector get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const; void set_global_filament_map_mode(FilamentMapMode mode); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index ca115b9773..8f3ac17f7c 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -79,7 +79,7 @@ public: Bind(wxEVT_LEFT_DOWN, &WikiLabel::OnLeftDown, this); } - void SetLabel(const wxString& label) + void SetLabel(const wxString& label) override { m_label = label; m_last_wrap_width = -1; // force re-wrap diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index 988d4c8171..6f55c0d953 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -163,7 +163,7 @@ public: BedType bedType() const { return m_BedType; } virtual void init() override; - virtual std::map extendedInfo() const + virtual std::map extendedInfo() const override { return {{"bedType", std::to_string(static_cast(m_BedType))}, {"timeLapse", std::to_string(m_timeLapse)}, @@ -200,7 +200,7 @@ public: PrintHost* printhost); virtual void init() override; - virtual std::map extendedInfo() const; + virtual std::map extendedInfo() const override; private: static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test"; diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 7d724ae273..486be04243 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -4,6 +4,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Thread.hpp" #include "libslic3r/Color.hpp" +#include "FilamentBitmapUtils.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "GUI_Preview.hpp" @@ -5693,10 +5694,15 @@ void SelectMachineDialog::clone_thumbnail_data() { m_preview_colors_in_thumbnail.resize(m_materialList.size()); } while (iter != m_materialList.end()) { - int id = iter->first; Material * item = iter->second; MaterialItem *m = item->item; - m_preview_colors_in_thumbnail[id] = m->m_material_coloul; + // Orca: key the preview colours by filament slot, as m_cur_colors_in_thumbnail and + // SyncAmsInfoDialog already do, so recompute_mixed_slot_colors() below can look a mixed + // slot's component colours up by id (BBS keys this array by list position). + if (item->id >= m_preview_colors_in_thumbnail.size()) { + m_preview_colors_in_thumbnail.resize(item->id + 1); + } + m_preview_colors_in_thumbnail[item->id] = m->m_material_coloul; if (item->id < m_cur_colors_in_thumbnail.size()) { m_cur_colors_in_thumbnail[item->id] = m->m_ams_coloul; } @@ -5706,6 +5712,20 @@ void SelectMachineDialog::clone_thumbnail_data() { } iter++; } + + // Expand color arrays to cover mixed (virtual) slots and compute their blended colors + const auto& cfg = wxGetApp().preset_bundle->project_config; + size_t total = 0; + if (auto* opt = cfg.option("filament_is_mixed")) + total = opt->values.size(); + size_t target = std::max(total, m_cur_colors_in_thumbnail.size()); + if (m_cur_colors_in_thumbnail.size() < target) + m_cur_colors_in_thumbnail.resize(target); + if (m_preview_colors_in_thumbnail.size() < target) + m_preview_colors_in_thumbnail.resize(target); + recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg); + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + //copy data auto &data = m_cur_input_thumbnail_data; m_preview_thumbnail_data.reset(); @@ -5880,6 +5900,10 @@ void SelectMachineDialog::change_default_normal(int old_filament_id, wxColour te return; } } + // Recompute mixed slot colors after physical slot color change + const auto& cfg = wxGetApp().preset_bundle->project_config; + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + ThumbnailData& data = m_cur_input_thumbnail_data; ThumbnailData& no_light_data = m_cur_no_light_thumbnail_data; if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) { diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index fd326f7c85..46d6adf4f4 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -522,7 +522,7 @@ public: bool is_timeout(); int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path); void set_print_type(PrintFromType type) {m_print_type = type;}; - bool Show(bool show); + bool Show(bool show) override; void show_init(); bool do_ams_mapping(MachineObject *obj_,bool use_ams); bool get_ams_mapping_result(std::string& mapping_array_str, std::string& mapping_array_str2, std::string& ams_mapping_info) const; diff --git a/src/slic3r/GUI/SendToPrinter.hpp b/src/slic3r/GUI/SendToPrinter.hpp index 14493a1f20..87948b28c3 100644 --- a/src/slic3r/GUI/SendToPrinter.hpp +++ b/src/slic3r/GUI/SendToPrinter.hpp @@ -180,7 +180,7 @@ public: SendToPrinterDialog(Plater *plater = nullptr); ~SendToPrinterDialog(); - bool Show(bool show); + bool Show(bool show) override; bool is_timeout(); void on_rename_click(wxCommandEvent& event); void on_rename_enter(); diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index ea67ad5b31..9515ecc116 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -30,6 +30,7 @@ #include "DeviceCore/DevManager.h" #include "DeviceCore/DevMapping.h" #include "DeviceCore/DevStorage.h" +#include "FilamentBitmapUtils.hpp" using namespace Slic3r; using namespace Slic3r::GUI; @@ -2575,6 +2576,10 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list() m_materialList.clear(); m_filaments.clear(); + // Mixed-color slots are virtual: they never occupy a tray, so they must not appear as + // AMS sync targets. + auto* is_mixed_opt = preset_bundle->project_config.option("filament_is_mixed"); + bool use_double_extruder = get_is_double_extruder(); if (use_double_extruder) { const auto &project_config = preset_bundle->project_config; @@ -2592,6 +2597,8 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list() auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]); if (extruder >= materials.size() || extruder < 0 || extruder >= display_materials.size()) continue; + if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder]) + continue; if (contronal_index % SYNC_FLEX_GRID_COL == 0) { wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL); @@ -2793,6 +2800,10 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list() m_fix_materialList.clear(); m_fix_filaments.clear(); + // Mixed-color slots are virtual: they never occupy a tray, so they must not appear as + // AMS sync targets. + auto* is_mixed_opt = preset_bundle->project_config.option("filament_is_mixed"); + bool use_double_extruder = get_is_double_extruder(); if (use_double_extruder) { const auto &project_config = preset_bundle->project_config; @@ -2810,6 +2821,8 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list() auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]); if (extruder >= extruders.size() || extruder < 0 || extruder >= m_ams_combo_info.ams_filament_colors.size()) continue; + if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder]) + continue; if (contronal_index % SYNC_FLEX_GRID_COL == 0) { wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL); @@ -2931,6 +2944,20 @@ void SyncAmsInfoDialog::clone_thumbnail_data() iter++; } } + + // Expand color arrays to cover mixed (virtual) slots and compute their blended colors + const auto& cfg = wxGetApp().preset_bundle->project_config; + size_t total = 0; + if (auto* opt = cfg.option("filament_is_mixed")) + total = opt->values.size(); + size_t target = std::max(total, m_cur_colors_in_thumbnail.size()); + if (m_cur_colors_in_thumbnail.size() < target) + m_cur_colors_in_thumbnail.resize(target); + if (m_preview_colors_in_thumbnail.size() < target) + m_preview_colors_in_thumbnail.resize(target); + recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg); + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + // copy data auto &data = m_cur_input_thumbnail_data; m_preview_thumbnail_data.reset(); @@ -3119,6 +3146,10 @@ void SyncAmsInfoDialog::change_default_normal(int old_filament_id, wxColour temp return; } } + // Recompute mixed slot colors after physical slot color change + const auto& cfg = wxGetApp().preset_bundle->project_config; + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + ThumbnailData &data = m_cur_input_thumbnail_data; ThumbnailData &no_light_data = m_cur_no_light_thumbnail_data; if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) { diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.hpp b/src/slic3r/GUI/SyncAmsInfoDialog.hpp index 8ff8f18aff..248ca4032c 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.hpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.hpp @@ -371,7 +371,7 @@ public: }; FinishSyncAmsDialog(InputInfo &input_info); ~FinishSyncAmsDialog() override; - void deal_ok(); + void deal_ok() override; void update_info(InputInfo& info); bool Layout() override; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 2b1341af6a..750beb0f1c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4,6 +4,7 @@ #include "PresetHints.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -2181,8 +2182,11 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); new_colors.push_back(new_color); } - wxGetApp().preset_bundle->set_num_filaments(num_extruder, new_colors); - wxGetApp().plater()->on_filament_count_change(num_extruder); + // Mixed-color slots are virtual filaments at the tail of the list with no nozzle of their + // own, so they are carried on top of the new extruder count instead of being truncated. + const size_t total_filaments = num_extruder + wxGetApp().preset_bundle->num_mixed_filaments(); + wxGetApp().preset_bundle->set_num_filaments(total_filaments, new_colors); + wxGetApp().plater()->on_filament_count_change(total_filaments); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); } @@ -2629,6 +2633,7 @@ void TabPrint::build() auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height"); optgroup->append_single_option_line("layer_height","quality_settings_layer_height"); optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height"); + optgroup->append_single_option_line("enable_mixed_color_sublayer"); optgroup = page->new_optgroup(L("Line width"), L"param_line_width"); optgroup->append_single_option_line("line_width","quality_settings_line_width"); @@ -3497,6 +3502,21 @@ void TabPrintModel::activate_selected_page(std::function throw_if_cancel f->set_value(boost::any(), false); } } + if (m_type == Preset::TYPE_PLATE) + static_cast(this)->update_mixed_filament_seq_state(); +} + +// A mixed-color slot resolves to a different physical filament per layer, so a +// user-defined filament print order cannot be honoured while one exists. +void TabPrintPlate::update_mixed_filament_seq_state() +{ + if (!m_active_page) return; + auto &proj_cfg = m_preset_bundle->project_config; + auto *opt = proj_cfg.option("filament_is_mixed"); + bool has_mixed = opt && has_any_mixed_filament(opt->values); + + toggle_option("first_layer_sequence_choice", !has_mixed); + toggle_option("other_layers_sequence_choice", !has_mixed); } void TabPrintModel::on_value_change(const std::string& opt_id, const boost::any& value) diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 1d3f8daf2a..52a715913f 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -514,13 +514,13 @@ public: bool has_key(std::string const &key); protected: - virtual void activate_selected_page(std::function throw_if_canceled); + virtual void activate_selected_page(std::function throw_if_canceled) override; virtual void on_value_change(const std::string& opt_key, const boost::any& value) override; virtual void notify_changed(ObjectBase * object) = 0; - virtual void reload_config(); + virtual void reload_config() override; virtual void update_custom_dirty(std::vector &dirty_options, std::vector &nonsys_options) override; @@ -544,6 +544,8 @@ public: void build() override; void reset_model_config() override; int show_spiral_mode_settings_dialog(bool is_object_config) { return m_config_manipulation.show_spiral_mode_settings_dialog(is_object_config); } + // Disables the user-defined filament print order while a mixed-color filament exists. + void update_mixed_filament_seq_state(); protected: virtual void on_value_change(const std::string& opt_key, const boost::any& value) override; diff --git a/src/slic3r/GUI/TabButton.hpp b/src/slic3r/GUI/TabButton.hpp index 7accf248c4..05ce1c6bd3 100644 --- a/src/slic3r/GUI/TabButton.hpp +++ b/src/slic3r/GUI/TabButton.hpp @@ -40,7 +40,7 @@ public: void SetBitmap(ScalableBitmap &bitmap); - bool Enable(bool enable = true); + bool Enable(bool enable = true) override; void Rescale(); diff --git a/src/slic3r/GUI/Tabbook.hpp b/src/slic3r/GUI/Tabbook.hpp index 7f10e9dd8d..87fd215327 100644 --- a/src/slic3r/GUI/Tabbook.hpp +++ b/src/slic3r/GUI/Tabbook.hpp @@ -166,7 +166,7 @@ public: return true; } - bool RemovePage(size_t n) + bool RemovePage(size_t n) override { if (!wxBookCtrlBase::RemovePage(n)) return false; diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp new file mode 100644 index 0000000000..1a24799a15 --- /dev/null +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -0,0 +1,4361 @@ +#include +#include "OpenGLManager.hpp" + +#include "TextureImportDialog.hpp" +#include "I18N.hpp" +#include "GUI_App.hpp" +#include "MsgDialog.hpp" +#include "ColorDecomposeDialog.hpp" +#include "ColorDecomposeSupport.hpp" +#include "Widgets/StateColor.hpp" +#include "Widgets/StaticLine.hpp" +#include "libslic3r/ColorDecomposeRecipe.hpp" +#include "libslic3r/MeshBoolean.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE = "PLA Basic"; +static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_SHORT_TYPE = "PLA"; +static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_NAME = "Bambu PLA Basic"; + +static bool is_dark() { return Slic3r::GUI::wxGetApp().dark_mode(); } + +static wxColour texture_import_gray9000() +{ + return wxColour(38, 46, 48); +} + +static wxColour texture_import_text_colour() +{ + return StateColor::darkModeColorFor(texture_import_gray9000()); +} + +// StaticLine::SetLineColour stores the raw key and resolves it itself when it paints, so those +// sinks take SEPARATOR_COLOUR_KEY directly; only raw wx sinks need the resolved form below. +static constexpr const char* SEPARATOR_COLOUR_KEY = "#CECECE"; + +static wxColour texture_import_separator_colour() +{ + return StateColor::darkModeColorFor(wxColour(SEPARATOR_COLOUR_KEY)); +} + +// Orca's confirm palette, applied here rather than through Button::SetStyle because these buttons +// keep custom pill geometry that SetStyle resets. The Disabled entries are load-bearing: without +// one, StateColor::colorForStates falls through to the Normal entry and a disabled button paints +// as a live accent button. +static void apply_accent_button_colours(Button* btn) +{ + btn->SetBackgroundColor(StateColor( + std::pair(wxColour("#CECECE"), StateColor::Disabled), + std::pair(wxColour(0, 137, 123), StateColor::Pressed), + std::pair(wxColour(38, 166, 154), StateColor::Hovered), + std::pair(wxColour(0, 150, 136), StateColor::Normal))); + btn->SetBorderColor(StateColor( + std::pair(wxColour("#CECECE"), StateColor::Disabled), + std::pair(wxColour(0, 150, 136), StateColor::Normal))); + btn->SetTextColor(StateColor( + std::pair(wxColour("#6B6B6A"), StateColor::Disabled), + std::pair(wxColour("#FFFFFE"), StateColor::Normal))); +} + +// The same button while the parameters behind it are dirty: still clickable, but reading as +// "what you see is not what this button would apply". +static void apply_muted_button_colours(Button* btn) +{ + btn->SetBackgroundColor(wxColour("#CECECE")); + btn->SetBorderColor(wxColour("#CECECE")); + btn->SetTextColor(wxColour("#6B6B6A")); +} + +static wxFont texture_import_section_title_font(wxWindow* win) +{ + wxFont font = win ? win->GetFont() : wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + font.MakeBold(); + return font; +} + +static wxSize gl_viewport_size(wxWindow* win, const wxSize& logical_size) +{ + wxSize viewport_size = logical_size; +#ifdef __APPLE__ + const double scale = win ? win->GetContentScaleFactor() : 1.0; + if (scale > 0.0) { + viewport_size.x = std::max(1, (int)std::round(viewport_size.x * scale)); + viewport_size.y = std::max(1, (int)std::round(viewport_size.y * scale)); + } +#else + (void)win; +#endif + return viewport_size; +} + +class ScopedInteractiveBusyCursorSuspender +{ +public: + ScopedInteractiveBusyCursorSuspender() + { +#if defined(__WXMSW__) || defined(__APPLE__) + while (wxIsBusy()) { + wxEndBusyCursor(); + ++m_suspended_count; + } +#endif + } + + ~ScopedInteractiveBusyCursorSuspender() + { +#if defined(__WXMSW__) || defined(__APPLE__) + for (int i = 0; i < m_suspended_count; ++i) + wxBeginBusyCursor(); +#endif + } + +private: + int m_suspended_count = 0; +}; + +static bool needs_filament_swatch_border(const wxColour& colour) +{ + if (is_dark()) + return colour.Red() < 45 && colour.Green() < 45 && colour.Blue() < 45; + return colour.Red() > 224 && colour.Green() > 224 && colour.Blue() > 224; +} + +static wxColour filament_swatch_border_colour() +{ + return is_dark() ? wxColour(207, 207, 207) : wxColour(130, 130, 128); +} + +static void draw_filament_swatch_border(wxDC& dc, const wxColour& colour, + int x, int y, int w, int h, int radius = 0) +{ + if (!needs_filament_swatch_border(colour)) + return; + + dc.SetPen(wxPen(filament_swatch_border_colour(), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + if (radius > 0) + dc.DrawRoundedRectangle(x, y, w, h, radius); + else + dc.DrawRectangle(x, y, w, h); +} + +static void draw_filament_swatch_ellipse_border(wxDC& dc, const wxColour& colour, + int x, int y, int w, int h) +{ + if (!needs_filament_swatch_border(colour)) + return; + + dc.SetPen(wxPen(filament_swatch_border_colour(), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawEllipse(x, y, w, h); +} + +static wxString ellipsize_text(wxDC& dc, wxString text, int max_width) +{ + if (max_width <= 0) + return wxEmptyString; + if (dc.GetTextExtent(text).x <= max_width) + return text; + + const wxString ellipsis = "..."; + while (!text.empty() && dc.GetTextExtent(text + ellipsis).x > max_width) + text.RemoveLast(); + if (text.empty() && dc.GetTextExtent(ellipsis).x > max_width) + return wxString(); + return text + ellipsis; +} + +// ============================================================ +// AccentSlider — thin track + accent-coloured triangle thumb +// ============================================================ + +class AccentSlider : public wxPanel { +public: + AccentSlider(wxWindow* parent, int value, int minVal, int maxVal, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize); + ~AccentSlider() override; + int GetValue() const; + void SetValue(int val); + bool Enable(bool enable = true) override; +private: + void OnPaint(wxPaintEvent&); + void OnMouse(wxMouseEvent&); + int xFromValue() const; + int valueFromX(int x) const; + int m_value, m_min, m_max; + bool m_dragging = false; +}; + +AccentSlider::AccentSlider(wxWindow* parent, int value, int minVal, int maxVal, + const wxPoint& pos, const wxSize& size) + : wxPanel(parent, wxID_ANY, pos, size.IsFullySpecified() ? size : wxSize(-1, parent->FromDIP(24)), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE) + , m_value(std::clamp(value, minVal, maxVal)), m_min(minVal), m_max(maxVal) +{ + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(-1, FromDIP(24))); + + Bind(wxEVT_PAINT, &AccentSlider::OnPaint, this); + Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) { + evt.Skip(); + Refresh(); + }); + Bind(wxEVT_LEFT_DOWN, &AccentSlider::OnMouse, this); + Bind(wxEVT_LEFT_UP, &AccentSlider::OnMouse, this); + Bind(wxEVT_MOTION, &AccentSlider::OnMouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { m_dragging = false; }); +} + +AccentSlider::~AccentSlider() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); +} + +int AccentSlider::GetValue() const { return m_value; } + +void AccentSlider::SetValue(int val) +{ + val = std::clamp(val, m_min, m_max); + if (val != m_value) { m_value = val; Refresh(); } +} + +bool AccentSlider::Enable(bool enable) +{ + bool ok = wxPanel::Enable(enable); + Refresh(); + return ok; +} + +int AccentSlider::xFromValue() const +{ + wxSize sz = GetClientSize(); + int margin = FromDIP(6); + int track_w = sz.x - 2 * margin; + if (m_max <= m_min || track_w <= 0) return margin; + return margin + (m_value - m_min) * track_w / (m_max - m_min); +} + +int AccentSlider::valueFromX(int x) const +{ + wxSize sz = GetClientSize(); + int margin = FromDIP(6); + int track_w = sz.x - 2 * margin; + if (track_w <= 0 || m_max <= m_min) return m_min; + int val = m_min + (x - margin) * (m_max - m_min) / track_w; + return std::clamp(val, m_min, m_max); +} + +void AccentSlider::OnPaint(wxPaintEvent&) +{ + wxAutoBufferedPaintDC dc(this); + wxSize sz = GetClientSize(); + + dc.SetBackground(wxBrush(GetParent()->GetBackgroundColour())); + dc.Clear(); + + int margin = FromDIP(6); + int track_y = sz.y / 2; + int ts = FromDIP(8); + int pen_w = FromDIP(2); + + wxColour accent_clr = StateColor::darkModeColorFor(IsEnabled() ? wxColour("#009688") : wxColour("#ACACAC")); + wxColour track_clr = StateColor::darkModeColorFor(IsEnabled() ? wxColour("#CECECE") : wxColour("#DFDFDF")); + + int tx = xFromValue(); + + dc.SetPen(wxPen(accent_clr, pen_w)); + dc.DrawLine(margin, track_y, tx, track_y); + + dc.SetPen(wxPen(track_clr, pen_w)); + dc.DrawLine(tx, track_y, sz.x - margin, track_y); + + wxPoint tri[3] = { + {tx, track_y + FromDIP(1)}, + {tx - ts / 2, track_y + FromDIP(1) + ts}, + {tx + ts / 2, track_y + FromDIP(1) + ts} + }; + dc.SetBrush(wxBrush(accent_clr)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawPolygon(3, tri); +} + +void AccentSlider::OnMouse(wxMouseEvent& evt) +{ + if (!IsEnabled()) return; + + auto update = [&](int x) { + int nv = valueFromX(x); + if (nv != m_value) { + m_value = nv; + Refresh(); + wxCommandEvent e(wxEVT_SLIDER, GetId()); + e.SetEventObject(this); + ProcessWindowEvent(e); + } + }; + + if (evt.LeftDown()) { + m_dragging = true; + if (!HasCapture()) CaptureMouse(); + update(evt.GetX()); + } else if (evt.LeftUp()) { + m_dragging = false; + if (HasCapture()) ReleaseMouse(); + } else if (evt.Dragging() && m_dragging) { + update(evt.GetX()); + } +} + +namespace Slic3r { namespace GUI { + +wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent); +wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent); +wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent); +wxDEFINE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent); + +static std::array parse_color_string(const std::string& hex) +{ + std::array c = {1.f, 1.f, 1.f, 1.f}; + if (hex.size() >= 7 && hex[0] == '#') { + unsigned long val = std::strtoul(hex.c_str() + 1, nullptr, 16); + c[0] = ((val >> 16) & 0xFF) / 255.f; + c[1] = ((val >> 8) & 0xFF) / 255.f; + c[2] = ((val ) & 0xFF) / 255.f; + } + return c; +} + +static wxString rgb_to_hex(const std::array& c) +{ + return wxString::Format("#%02X%02X%02X", + (unsigned)c[0], (unsigned)c[1], (unsigned)c[2]); +} + +static wxString filament_name_to_wx_string(const std::string& name) +{ + wxString utf8_name = wxString::FromUTF8(name.c_str()); + if (!utf8_name.empty() || name.empty()) + return utf8_name; + return wxString(name); +} + +static std::string texture_normalize_color_hex(std::string hex) +{ + if (hex.empty()) + return "#808080"; + if (hex.front() != '#') + hex = "#" + hex; + return decompose_normalize_color_hex(std::move(hex)); +} + +static std::string texture_rgba_to_hex(const std::array& rgba) +{ + return wxString::Format("#%02X%02X%02X", + (unsigned char)std::clamp(rgba[0] * 255.f, 0.f, 255.f), + (unsigned char)std::clamp(rgba[1] * 255.f, 0.f, 255.f), + (unsigned char)std::clamp(rgba[2] * 255.f, 0.f, 255.f)).ToStdString(); +} + +static bool texture_entry_is_physical(TextureFilamentKind kind) +{ + return kind == TextureFilamentKind::ExistingPhysical || kind == TextureFilamentKind::NewPhysical; +} + +static bool texture_entry_is_mixed(TextureFilamentKind kind) +{ + return kind == TextureFilamentKind::ExistingMixed || kind == TextureFilamentKind::NewMixed; +} + +static bool texture_entry_is_pla_basic(const TextureFilamentEntry& entry) +{ + return entry.type == DEFAULT_VIRTUAL_FILAMENT_SHORT_TYPE || entry.type == DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE || + entry.name.find(DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE) != std::string::npos || + entry.preset_name.find(DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE) != std::string::npos; +} + +static bool texture_entry_official_basic(const TextureFilamentEntry& entry) +{ + if (!texture_entry_is_physical(entry.kind)) + return false; + // NewPhysical entries are created by add_virtual_filament with a fixed Bambu Basic name. + if (entry.kind == TextureFilamentKind::NewPhysical) + return !official_basic_type_from_preset_name(entry.name).empty(); + // ExistingPhysical: resolve the filament preset name from project_config_index. + auto& pb = *wxGetApp().preset_bundle; + const size_t cfg = entry.project_config_index; + if (cfg < pb.filament_presets.size()) + return !official_basic_type_from_preset_name(pb.filament_presets[cfg]).empty(); + return false; +} + +static Slic3r::ColorDecomposeRecipeMode texture_recipe_mode(TextureAutoMixMode mode) +{ + return mode == TextureAutoMixMode::CMYW ? Slic3r::ColorDecomposeRecipeMode::CMYW : + Slic3r::ColorDecomposeRecipeMode::RYBW; +} + +static bool starts_with_preset_name(const std::string& name, const char* prefix) +{ + const size_t prefix_len = std::strlen(prefix); + return name.size() >= prefix_len && name.compare(0, prefix_len, prefix) == 0; +} + +static std::string resolve_default_virtual_filament_preset_name() +{ + auto* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return {}; + + auto valid_preset_name = [preset_bundle](const std::string& name) -> bool { + return !name.empty() && preset_bundle->filaments.find_preset(name, false) != nullptr; + }; + + const auto* default_profiles = preset_bundle->printers.get_selected_preset() + .config.option("default_filament_profile"); + if (default_profiles) { + for (const std::string& name : default_profiles->values) { + if (starts_with_preset_name(name, DEFAULT_VIRTUAL_FILAMENT_NAME) && valid_preset_name(name)) + return name; + } + } + + for (const Preset& preset : preset_bundle->filaments.get_presets()) { + if (preset.is_system && preset.is_visible && preset.is_compatible && + starts_with_preset_name(preset.name, DEFAULT_VIRTUAL_FILAMENT_NAME)) { + return preset.name; + } + } + + for (const Preset& preset : preset_bundle->filaments.get_presets()) { + if (preset.is_visible && preset.is_compatible && + starts_with_preset_name(preset.name, DEFAULT_VIRTUAL_FILAMENT_NAME)) { + return preset.name; + } + } + + std::string selected = preset_bundle->filaments.get_selected_preset_name(); + return valid_preset_name(selected) ? selected : std::string(); +} + +static wxString auto_mix_mode_label(TextureAutoMixMode mode) +{ + return mode == TextureAutoMixMode::CMYW ? _L("One-click CMYW auto-mix") : + _L("One-click RYBW auto-mix"); +} + +static wxPoint constrained_dialog_position(wxWindow* anchor, const wxSize& dialog_size) +{ + if (!anchor) + return wxDefaultPosition; + + wxSize size = dialog_size; + if (size.x <= 0 || size.y <= 0) + size = wxSize(anchor->FromDIP(450), anchor->FromDIP(350)); + + wxPoint pos = anchor->ClientToScreen(wxPoint(0, anchor->GetSize().y)); + wxRect display_rect; + int display_idx = wxDisplay::GetFromPoint(pos); + if (display_idx != wxNOT_FOUND) + display_rect = wxDisplay(display_idx).GetClientArea(); + else + display_rect = wxDisplay().GetClientArea(); + + pos.x = std::clamp(pos.x, display_rect.GetLeft(), + std::max(display_rect.GetLeft(), display_rect.GetRight() - size.x)); + pos.y = std::clamp(pos.y, display_rect.GetTop(), + std::max(display_rect.GetTop(), display_rect.GetBottom() - size.y)); + return pos; +} + +// ============================================================ +// FilamentSelectPopup +// ============================================================ + +class FilamentSelectPopup : public PopupWindow +{ +public: + FilamentSelectPopup(wxWindow* parent, + const std::vector& entries, + const std::vector>& colors_rgba, + const std::vector& names, + size_t existing_count, + int popup_width, + wxWindow* dialog_anchor, + std::function on_select, + std::function on_add_filament, + std::function on_decompose_color, + std::function can_add_filament, + std::function on_close, + std::vector display_numbers) + : PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + , m_entries(entries) + , m_colors_rgba(colors_rgba) + , m_names(names) + , m_existing_count(existing_count) + , m_dialog_anchor(dialog_anchor) + , m_on_select(std::move(on_select)) + , m_on_add_filament(std::move(on_add_filament)) + , m_on_decompose_color(std::move(on_decompose_color)) + , m_can_add_filament(std::move(can_add_filament)) + , m_on_close(std::move(on_close)) + , m_display_numbers(std::move(display_numbers)) + { + wxColour pop_bg = StateColor::darkModeColorFor(*wxWHITE); + SetBackgroundColour(pop_bg); + + m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + m_content->SetBackgroundColour(pop_bg); + m_content->SetScrollRate(0, FromDIP(5)); + auto* outer = new wxBoxSizer(wxVERTICAL); + + const int pop_w = std::max(FromDIP(213), popup_width); + const int row_h = FromDIP(32); + const int pad = FromDIP(8); + const int max_visible_rows = 10; + const wxColour header_clr = StateColor::darkModeColorFor(wxColour("#ACACAC")); + + auto add_section_header = [&](const wxString& label) { + auto* hdr = new wxStaticText(m_content, wxID_ANY, label); + wxFont hf = hdr->GetFont(); + hf.SetPointSize(9); + hdr->SetFont(hf); + hdr->SetForegroundColour(header_clr); + outer->Add(hdr, 0, wxLEFT | wxRIGHT | wxTOP, pad); + auto* line = new StaticLine(m_content); + line->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + outer->Add(line, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + }; + + auto add_section = [&](const wxString& label, TextureFilamentKind kind) { + bool has_any = false; + for (const auto& entry : m_entries) { + if (entry.kind == kind) { + has_any = true; + break; + } + } + if (!has_any) + return; + add_section_header(label); + for (const auto& entry : m_entries) { + if (entry.kind != kind) + continue; + wxPanel* row = texture_entry_is_mixed(entry.kind) ? create_mixed_item_row(entry, row_h) + : create_item_row((size_t)entry.dialog_index, row_h); + outer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT, pad); + } + }; + + // Section order matches compute_display_numbers() so the visible IDs + // ascend monotonically (ExistingPhysical -> NewPhysical -> ExistingMixed + // -> NewMixed) instead of jumping (e.g. 1,2 -> 7 -> 3,4,5,6 -> 8,9,10). + add_section(_L("Project Physical Filaments"), TextureFilamentKind::ExistingPhysical); + add_section(_L("New Physical Filaments"), TextureFilamentKind::NewPhysical); + add_section(_L("Project Mixed Filaments"), TextureFilamentKind::ExistingMixed); + add_section(_L("New Mixed Filaments"), TextureFilamentKind::NewMixed); + + auto* decompose_label = new wxStaticText(this, wxID_ANY, _L("Decompose Color")); + auto* add_label = new wxStaticText(this, wxID_ANY, _L("+ Add Material")); + wxFont af = add_label->GetFont(); + af.SetPointSize(10); + add_label->SetFont(af); + decompose_label->SetFont(af); + const bool add_enabled = !m_can_add_filament || m_can_add_filament(); + const wxColour action_clr = StateColor::darkModeColorFor(wxColour("#009688")); + add_label->SetForegroundColour(add_enabled ? action_clr : header_clr); + decompose_label->SetForegroundColour(add_enabled ? action_clr : header_clr); + add_label->SetCursor(wxCursor(add_enabled ? wxCURSOR_HAND : wxCURSOR_ARROW)); + decompose_label->SetCursor(wxCursor(add_enabled ? wxCURSOR_HAND : wxCURSOR_ARROW)); + if (!add_enabled) + add_label->SetToolTip(wxString::Format( + _L("The project supports up to %d filaments. Extra filaments will be discarded."), + (int)EnforcerBlockerType::ExtruderMax)); + decompose_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { + if (m_can_add_filament && !m_can_add_filament()) + return; + auto on_decompose_color = m_on_decompose_color; + m_closing_from_action = true; + Dismiss(); + if (on_decompose_color) + on_decompose_color(); + }); + add_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { + if (m_can_add_filament && !m_can_add_filament()) { + return; + } + auto on_add_filament = m_on_add_filament; + wxWindow* popup_parent = GetParent(); + wxWindow* color_anchor = m_dialog_anchor ? m_dialog_anchor : popup_parent; + m_closing_from_action = true; + Dismiss(); + wxColourData cd; + cd.SetChooseFull(true); + wxColourDialog dlg(popup_parent, &cd); + auto move_color_dialog = [&dlg, color_anchor]() { + dlg.Move(constrained_dialog_position(color_anchor, dlg.GetBestSize())); + }; + dlg.Bind(wxEVT_SHOW, [move_color_dialog](wxShowEvent& e) mutable { + e.Skip(); + if (e.IsShown()) + move_color_dialog(); + }); + move_color_dialog(); + if (dlg.ShowModal() == wxID_OK) { + wxColour clr = dlg.GetColourData().GetColour(); + if (on_add_filament) on_add_filament(clr); + } + }); + + m_content->SetSizer(outer); + m_content->FitInside(); + + auto* top_sizer = new wxBoxSizer(wxVERTICAL); + int list_h = outer->GetMinSize().y; + if (m_colors_rgba.size() > max_visible_rows) + list_h -= ((int)m_colors_rgba.size() - max_visible_rows) * row_h; + m_content->SetMinSize(wxSize(pop_w, list_h)); + m_content->SetMaxSize(wxSize(pop_w, list_h)); + top_sizer->Add(m_content, 0, wxEXPAND); + + top_sizer->AddSpacer(FromDIP(4)); + auto* sep_line = new StaticLine(this); + sep_line->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + top_sizer->Add(sep_line, 0, wxEXPAND | wxLEFT | wxRIGHT, pad); + top_sizer->Add(decompose_label, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + auto* sep_line2 = new StaticLine(this); + sep_line2->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + top_sizer->Add(sep_line2, 0, wxEXPAND | wxLEFT | wxRIGHT, pad); + top_sizer->Add(add_label, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + SetSizerAndFit(top_sizer); + + SetSize(pop_w, top_sizer->GetMinSize().y); + } + +private: + void OnDismiss() override + { + restore_cursor_state(); + if (m_on_close) m_on_close(m_closing_from_action); + m_closing_from_action = false; + wxPopupTransientWindow::OnDismiss(); + schedule_destroy(); + } + + void restore_cursor_state() + { + SetCursor(wxNullCursor); + if (m_content) + m_content->SetCursor(wxNullCursor); + if (m_dialog_anchor) + m_dialog_anchor->SetCursor(wxCursor(wxCURSOR_HAND)); + wxSetCursor(wxNullCursor); + } + + void schedule_destroy() + { + if (m_destroy_scheduled) + return; + m_destroy_scheduled = true; + CallAfter([this]() { Destroy(); }); + } + + wxPanel* create_item_row(size_t idx, int row_h) + { + wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE); + wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4")); + wxColour name_fg = texture_import_text_colour(); + + wxPanel* row = new wxPanel(m_content, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h)); + row->SetBackgroundColour(row_bg); + row->SetBackgroundStyle(wxBG_STYLE_PAINT); + row->SetCursor(wxCursor(wxCURSOR_HAND)); + + const int sq = row->FromDIP(24); + const int sq_r = row->FromDIP(2); + const int sq_x = row->FromDIP(4); + const int gap1 = row->FromDIP(8); + + wxColour fil_clr = idx < m_colors_rgba.size() + ? wxColour((unsigned char)(m_colors_rgba[idx][0] * 255.f), + (unsigned char)(m_colors_rgba[idx][1] * 255.f), + (unsigned char)(m_colors_rgba[idx][2] * 255.f)) + : wxColour(128, 128, 128); + + wxString name_str = (idx < m_names.size()) ? filament_name_to_wx_string(m_names[idx]) + : wxString::Format("Filament %d", display_number((int)idx)); + row->SetToolTip(name_str); + + row->Bind(wxEVT_PAINT, [this, idx, sq, sq_r, sq_x, gap1, fil_clr, name_str, row_bg, hover_bg, name_fg](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + bool hovered = (m_hover_idx == (int)idx); + dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + int sq_y = (sz.y - sq) / 2; + wxColour paint_clr = fil_clr; + if (idx < m_colors_rgba.size()) { + paint_clr = wxColour((unsigned char)(m_colors_rgba[idx][0] * 255.f), + (unsigned char)(m_colors_rgba[idx][1] * 255.f), + (unsigned char)(m_colors_rgba[idx][2] * 255.f)); + } + dc.SetBrush(wxBrush(paint_clr)); + dc.DrawRoundedRectangle(sq_x, sq_y, sq, sq, sq_r); + draw_filament_swatch_border(dc, paint_clr, sq_x, sq_y, sq, sq, sq_r); + + { + wxFont nf = p->GetFont(); + nf.SetPointSize(9); + dc.SetFont(nf); + dc.SetTextForeground(paint_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + wxString ns = wxString::Format("%d", display_number((int)idx)); + wxSize tsz = dc.GetTextExtent(ns); + dc.DrawText(ns, sq_x + (sq - tsz.x) / 2, sq_y + (sq - tsz.y) / 2); + } + + // Material name + { + wxFont mf = p->GetFont(); + mf.SetPointSize(10); + dc.SetFont(mf); + dc.SetTextForeground(name_fg); + int tx = sq_x + sq + gap1; + wxString display = ellipsize_text(dc, name_str, sz.x - tx - p->FromDIP(4)); + wxSize tsz = dc.GetTextExtent(display); + if (!display.empty()) + dc.DrawText(display, tx, (sz.y - tsz.y) / 2); + } + }); + + row->Bind(wxEVT_MOTION, [this, idx](wxMouseEvent& evt) { + if (m_hover_idx != (int)idx) { + m_hover_idx = (int)idx; + m_content->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { + if (m_hover_idx != -1) { + m_hover_idx = -1; + m_content->Refresh(); + } + evt.Skip(); + }); + + row->Bind(wxEVT_LEFT_DOWN, [this, idx](wxMouseEvent&) { + if (m_on_select) m_on_select((int)idx); + m_closing_from_action = true; + Dismiss(); + }); + + return row; + } + + wxPanel* create_mixed_item_row(const TextureFilamentEntry& entry, int row_h) + { + wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE); + wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4")); + wxColour name_fg = texture_import_text_colour(); + const int idx = entry.dialog_index; + + wxPanel* row = new wxPanel(m_content, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h)); + row->SetBackgroundColour(row_bg); + row->SetBackgroundStyle(wxBG_STYLE_PAINT); + row->SetCursor(wxCursor(wxCURSOR_HAND)); + row->SetToolTip(entry.name.empty() ? wxString::Format("Filament %d", display_number(idx)) : filament_name_to_wx_string(entry.name)); + + row->Bind(wxEVT_PAINT, [this, entry, idx, row_bg, hover_bg, name_fg](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + const bool hovered = (m_hover_idx == idx); + dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + wxFont font = p->GetFont(); + font.SetPointSize(9); + dc.SetFont(font); + int x = p->FromDIP(2); + const int sw = p->FromDIP(22); + const int sw_r = p->FromDIP(2); + const int y = (sz.y - sw) / 2; + + for (size_t ci = 0; ci < entry.mixed_components.size() && ci < entry.mixed_ratios.size(); ++ci) { + if (ci > 0) { + dc.SetTextForeground(name_fg); + wxString plus = "+"; + wxSize psz = dc.GetTextExtent(plus); + dc.DrawText(plus, x, (sz.y - psz.y) / 2); + x += psz.x + p->FromDIP(4); + } + + const unsigned int comp_id = entry.mixed_components[ci]; + const int comp_dialog_idx = comp_id >= 1 ? (int)comp_id - 1 : -1; + wxColour comp_clr("#D9D9D9"); + if (comp_dialog_idx >= 0 && comp_dialog_idx < (int)m_colors_rgba.size()) { + const auto& c = m_colors_rgba[comp_dialog_idx]; + comp_clr = wxColour((unsigned char)(c[0] * 255.f), + (unsigned char)(c[1] * 255.f), + (unsigned char)(c[2] * 255.f)); + } + + dc.SetBrush(wxBrush(comp_clr)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRoundedRectangle(x, y, sw, sw, sw_r); + draw_filament_swatch_border(dc, comp_clr, x, y, sw, sw, sw_r); + + wxString num = wxString::Format("%d", display_number(comp_dialog_idx)); + wxSize nsz = dc.GetTextExtent(num); + dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + dc.DrawText(num, x + (sw - nsz.x) / 2, y + (sw - nsz.y) / 2); + x += sw + p->FromDIP(4); + + dc.SetTextForeground(name_fg); + wxString pct = wxString::Format("%d%%", entry.mixed_ratios[ci]); + wxSize psz = dc.GetTextExtent(pct); + dc.DrawText(pct, x, (sz.y - psz.y) / 2); + x += psz.x + p->FromDIP(4); + } + }); + + row->Bind(wxEVT_MOTION, [this, idx](wxMouseEvent& evt) { + if (m_hover_idx != idx) { + m_hover_idx = idx; + m_content->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { + if (m_hover_idx != -1) { + m_hover_idx = -1; + m_content->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEFT_DOWN, [this, idx](wxMouseEvent&) { + if (m_on_select) m_on_select(idx); + m_closing_from_action = true; + Dismiss(); + }); + + return row; + } + + wxScrolledWindow* m_content = nullptr; + std::vector m_entries; + std::vector> m_colors_rgba; + std::vector m_names; + size_t m_existing_count = 0; + wxWindow* m_dialog_anchor = nullptr; + std::function m_on_select; + std::function m_on_add_filament; + std::function m_on_decompose_color; + std::function m_can_add_filament; + std::function m_on_close; + // 1-based display number per dialog_index, mirroring the post-apply + // sidebar ordering (ExistingPhysical, NewPhysical, ExistingMixed, NewMixed). + std::vector m_display_numbers; + int m_hover_idx = -1; + bool m_closing_from_action = false; + bool m_destroy_scheduled = false; + + // Returns the display number for a dialog_index, falling back to idx + 1 + // when no mapping is available (e.g. index out of range). + int display_number(int idx) const { + return (idx >= 0 && idx < (int)m_display_numbers.size() && m_display_numbers[idx] > 0) + ? m_display_numbers[idx] : idx + 1; + } +}; + +// ============================================================ +// AutoMixSelectPopup +// ============================================================ + +class AutoMixSelectPopup : public PopupWindow +{ +public: + AutoMixSelectPopup(wxWindow* parent, + TextureAutoMixMode current_mode, + int popup_width, + int font_point_size, + std::function on_select, + std::function on_close) + : PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + , m_current_mode(current_mode) + , m_font_point_size(font_point_size) + , m_on_select(std::move(on_select)) + , m_on_close(std::move(on_close)) + { + wxColour pop_bg = StateColor::darkModeColorFor(*wxWHITE); + SetBackgroundColour(pop_bg); + + auto* content = new wxPanel(this, wxID_ANY); + content->SetBackgroundColour(pop_bg); + content->SetBackgroundStyle(wxBG_STYLE_PAINT); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + const int row_h = FromDIP(36); + const int pop_w = std::max(FromDIP(216), popup_width); + sizer->Add(create_item_row(content, TextureAutoMixMode::CMYW, row_h), 0, wxEXPAND); + sizer->Add(create_item_row(content, TextureAutoMixMode::RYBW, row_h), 0, wxEXPAND); + content->SetSizer(sizer); + content->SetMinSize(wxSize(pop_w, row_h * 2)); + + auto* top_sizer = new wxBoxSizer(wxVERTICAL); + top_sizer->Add(content, 0, wxEXPAND | wxALL, FromDIP(4)); + SetSizerAndFit(top_sizer); + SetSize(pop_w, top_sizer->GetMinSize().y); + } + +private: + void OnDismiss() override + { + if (m_on_close) + m_on_close(); + wxPopupTransientWindow::OnDismiss(); + CallAfter([this]() { Destroy(); }); + } + + wxPanel* create_item_row(wxWindow* parent, TextureAutoMixMode mode, int row_h) + { + wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE); + wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4")); + wxColour text_fg = texture_import_text_colour(); + wxColour accent = StateColor::darkModeColorFor(wxColour("#009688")); + + wxPanel* row = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE); + row->SetBackgroundColour(row_bg); + row->SetBackgroundStyle(wxBG_STYLE_PAINT); + row->SetCursor(wxCursor(wxCURSOR_HAND)); + + const int row_idx = mode == TextureAutoMixMode::CMYW ? 0 : 1; + row->Bind(wxEVT_PAINT, [this, row_bg, hover_bg, text_fg, accent, mode, row_idx](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + const bool hovered = (m_hover_idx == row_idx); + const bool selected = (m_current_mode == mode); + + dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + wxFont font = p->GetFont(); + font.SetPointSize(m_font_point_size); + dc.SetFont(font); + dc.SetTextForeground(text_fg); + wxString label = auto_mix_mode_label(mode); + wxSize tsz = dc.GetTextExtent(label); + dc.DrawText(label, p->FromDIP(12), (sz.y - tsz.y) / 2); + + if (selected) { + wxFont check_font = p->GetFont(); + check_font.SetPointSize(12); + check_font.MakeBold(); + dc.SetFont(check_font); + dc.SetTextForeground(accent); + wxString check = wxString::FromUTF8("✓"); + wxSize csz = dc.GetTextExtent(check); + dc.DrawText(check, sz.x - p->FromDIP(16) - csz.x, (sz.y - csz.y) / 2); + } + }); + + row->Bind(wxEVT_MOTION, [this, row, row_idx](wxMouseEvent& evt) { + if (m_hover_idx != row_idx) { + m_hover_idx = row_idx; + row->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEAVE_WINDOW, [this, row](wxMouseEvent& evt) { + m_hover_idx = -1; + row->Refresh(); + evt.Skip(); + }); + row->Bind(wxEVT_LEFT_DOWN, [this, mode](wxMouseEvent&) { + if (m_on_select) + m_on_select(mode); + Dismiss(); + }); + + return row; + } + + TextureAutoMixMode m_current_mode; + int m_font_point_size = 10; + int m_hover_idx = -1; + std::function m_on_select; + std::function m_on_close; +}; + +// ============================================================ +// TexturePreviewCanvas +// ============================================================ + +TexturePreviewCanvas::TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs) + : wxGLCanvas(parent, attrs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE) +{ + m_context = new wxGLContext(this); + + Bind(wxEVT_PAINT, &TexturePreviewCanvas::on_paint, this); + Bind(wxEVT_SIZE, &TexturePreviewCanvas::on_size, this); + Bind(wxEVT_MOUSEWHEEL, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_LEFT_DOWN, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_LEFT_UP, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_RIGHT_DOWN, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_RIGHT_UP, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MIDDLE_DOWN, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MIDDLE_UP, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MOTION, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_drag_mode = DragMode::None; + m_reset_overlay_pressed = false; + }); + Bind(wxEVT_LEAVE_WINDOW, &TexturePreviewCanvas::on_mouse, this); +} + +TexturePreviewCanvas::~TexturePreviewCanvas() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); + + if (m_context) { + SetCurrent(*m_context); + if (m_tex_id) + glDeleteTextures(1, &m_tex_id); + for (unsigned int id : m_gl_tex_ids) + if (id) glDeleteTextures(1, &id); + for (unsigned int id : {m_reset_icon_tex, m_reset_icon_hover_tex, + m_reset_icon_dark_tex, m_reset_icon_dark_hover_tex}) + if (id) glDeleteTextures(1, &id); + delete m_context; + } +} + +void TexturePreviewCanvas::set_mesh_data( + const std::vector>& vertices, + const std::vector>& indices) +{ + m_vertices = vertices; + m_indices = indices; + update_bounding_box(); + compute_smooth_normals(); + Refresh(); +} + +void TexturePreviewCanvas::compute_smooth_normals() +{ + m_vertex_normals.clear(); + if (m_vertices.empty() || m_indices.empty()) return; + + m_vertex_normals.resize(m_vertices.size(), {0.f, 0.f, 0.f}); + + for (const auto& face : m_indices) { + int i0 = face[0], i1 = face[1], i2 = face[2]; + if (i0 < 0 || i0 >= (int)m_vertices.size() || + i1 < 0 || i1 >= (int)m_vertices.size() || + i2 < 0 || i2 >= (int)m_vertices.size()) + continue; + + const auto& v0 = m_vertices[i0]; + const auto& v1 = m_vertices[i1]; + const auto& v2 = m_vertices[i2]; + + float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]); + float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]); + float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]); + + m_vertex_normals[i0][0] += nx; m_vertex_normals[i0][1] += ny; m_vertex_normals[i0][2] += nz; + m_vertex_normals[i1][0] += nx; m_vertex_normals[i1][1] += ny; m_vertex_normals[i1][2] += nz; + m_vertex_normals[i2][0] += nx; m_vertex_normals[i2][1] += ny; m_vertex_normals[i2][2] += nz; + } + + for (auto& n : m_vertex_normals) { + float len = std::sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]); + if (len > 1e-8f) { n[0] /= len; n[1] /= len; n[2] /= len; } + } +} + +void TexturePreviewCanvas::set_texture_data( + const std::vector>& uvs, + const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels) +{ + m_uvs = uvs; + m_tex_w = tex_w; + m_tex_h = tex_h; + m_tex_channels = tex_channels; + m_tex_dirty = true; + + size_t sz = (size_t)tex_w * tex_h * tex_channels; + m_tex_data.assign(tex_data, tex_data + sz); + Refresh(); +} + +void TexturePreviewCanvas::set_texture_render_data( + const std::vector>& tex_pixels_rgb, + const std::vector& tex_widths, + const std::vector& tex_heights, + const std::vector, 3>>& face_uvs, + const std::vector& face_tex_ids) +{ + m_tex_pixels_rgb = tex_pixels_rgb; + m_tex_widths = tex_widths; + m_tex_heights = tex_heights; + m_face_uvs = face_uvs; + m_face_tex_ids = face_tex_ids; + m_multi_tex_dirty = true; + Refresh(); +} + +void TexturePreviewCanvas::upload_textures() +{ + if (!m_multi_tex_dirty) return; + m_multi_tex_dirty = false; + + for (unsigned int id : m_gl_tex_ids) + if (id) glDeleteTextures(1, &id); + m_gl_tex_ids.clear(); + + m_gl_tex_ids.resize(m_tex_pixels_rgb.size(), 0); + for (size_t i = 0; i < m_tex_pixels_rgb.size(); ++i) { + if (m_tex_pixels_rgb[i].empty() || m_tex_widths[i] <= 0 || m_tex_heights[i] <= 0) + continue; + GLuint tex_id = 0; + glGenTextures(1, &tex_id); + glBindTexture(GL_TEXTURE_2D, tex_id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_tex_widths[i], m_tex_heights[i], + 0, GL_RGB, GL_UNSIGNED_BYTE, m_tex_pixels_rgb[i].data()); + m_gl_tex_ids[i] = tex_id; + } + glBindTexture(GL_TEXTURE_2D, 0); +} + +void TexturePreviewCanvas::set_painted_mesh_data( + const std::vector>& vertices, + const std::vector>& indices) +{ + m_painted_vertices = vertices; + m_painted_indices = indices; + Refresh(); +} + +static void convert_face_colors(const std::vector>& src, + std::vector>& dst) +{ + dst.resize(src.size()); + for (size_t i = 0; i < src.size(); ++i) + dst[i] = { src[i][0] / 255.f, src[i][1] / 255.f, src[i][2] / 255.f }; +} + +void TexturePreviewCanvas::set_face_colors(const std::vector>& face_colors) +{ + convert_face_colors(face_colors, m_face_colors_rgb); + Refresh(); +} + +void TexturePreviewCanvas::set_original_face_colors(const std::vector>& face_colors) +{ + convert_face_colors(face_colors, m_original_face_colors_rgb); + Refresh(); +} + +void TexturePreviewCanvas::set_filament_color_map( + const std::map, std::array>& color_map) +{ + m_color_map = color_map; + m_filament_colors_rgb.resize(m_face_colors_rgb.size()); + for (size_t i = 0; i < m_face_colors_rgb.size(); ++i) { + std::array key = { + (std::size_t)(m_face_colors_rgb[i][0] * 255.f + 0.5f), + (std::size_t)(m_face_colors_rgb[i][1] * 255.f + 0.5f), + (std::size_t)(m_face_colors_rgb[i][2] * 255.f + 0.5f) + }; + auto it = color_map.find(key); + if (it != color_map.end()) + m_filament_colors_rgb[i] = it->second; + else + m_filament_colors_rgb[i] = m_face_colors_rgb[i]; + } + Refresh(); +} + +void TexturePreviewCanvas::set_render_mode(RenderMode mode) +{ + if (m_mode != mode) { + m_mode = mode; + Refresh(); + } +} + +void TexturePreviewCanvas::set_computing_overlay(bool /*show*/) +{ + Refresh(); +} + +void TexturePreviewCanvas::reset_view() +{ + m_zoom = 1.0f; + m_rot_x = -30.0f; + m_rot_y = 30.0f; + m_pan_x = 0.0f; + m_pan_y = 0.0f; + Refresh(); +} + +wxRect TexturePreviewCanvas::reset_overlay_rect() const +{ + wxSize sz = GetClientSize(); + const int button_size = FromDIP(40); + const int margin = FromDIP(20); + return wxRect( + std::max(margin, sz.x - button_size - margin), + std::max(margin, sz.y - button_size - margin), + button_size, + button_size); +} + +unsigned int TexturePreviewCanvas::upload_reset_icon_texture(const std::string& icon_name) +{ + wxBitmap bmp = create_scaled_bitmap(icon_name, this, 40); + if (!bmp.IsOk()) + return 0; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk()) + return 0; + + const int w = image.GetWidth(); + const int h = image.GetHeight(); + const unsigned char* rgb = image.GetData(); + const unsigned char* alpha = image.HasAlpha() ? image.GetAlpha() : nullptr; + if (!rgb || w <= 0 || h <= 0) + return 0; + + std::vector rgba((size_t)w * h * 4); + for (int i = 0; i < w * h; ++i) { + rgba[(size_t)i * 4 + 0] = rgb[i * 3 + 0]; + rgba[(size_t)i * 4 + 1] = rgb[i * 3 + 1]; + rgba[(size_t)i * 4 + 2] = rgb[i * 3 + 2]; + rgba[(size_t)i * 4 + 3] = alpha ? alpha[i] : 255; + } + + GLuint tex_id = 0; + glGenTextures(1, &tex_id); + glBindTexture(GL_TEXTURE_2D, tex_id); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + glBindTexture(GL_TEXTURE_2D, 0); + return tex_id; +} + +void TexturePreviewCanvas::upload_reset_icon_textures() +{ + if (m_reset_icon_tex && m_reset_icon_hover_tex && m_reset_icon_dark_tex && m_reset_icon_dark_hover_tex) + return; + + if (!m_reset_icon_tex) + m_reset_icon_tex = upload_reset_icon_texture("canvas_zoom"); + if (!m_reset_icon_hover_tex) + m_reset_icon_hover_tex = upload_reset_icon_texture("canvas_zoom_hover"); + if (!m_reset_icon_dark_tex) + m_reset_icon_dark_tex = upload_reset_icon_texture("canvas_zoom_dark"); + if (!m_reset_icon_dark_hover_tex) + m_reset_icon_dark_hover_tex = upload_reset_icon_texture("canvas_zoom_dark_hover"); +} + +bool TexturePreviewCanvas::handle_reset_overlay_mouse(wxMouseEvent& evt) +{ + if (evt.Leaving()) { + if (m_reset_overlay_pressed) { + m_reset_overlay_hovered = false; + m_reset_overlay_pressed = false; + SetCursor(wxCursor(wxCURSOR_ARROW)); + if (HasCapture()) + ReleaseMouse(); + Refresh(); + return true; + } + if (m_reset_overlay_hovered) { + m_reset_overlay_hovered = false; + SetCursor(wxCursor(wxCURSOR_ARROW)); + Refresh(); + } + return false; + } + + const bool over = reset_overlay_rect().Contains(evt.GetPosition()); + if (over != m_reset_overlay_hovered) { + m_reset_overlay_hovered = over; + SetCursor(wxCursor(over ? wxCURSOR_HAND : wxCURSOR_ARROW)); + Refresh(); + } + + if (m_drag_mode != DragMode::None && !m_reset_overlay_pressed) + return false; + + if (evt.LeftDown() && over) { + m_reset_overlay_pressed = true; + if (!HasCapture()) + CaptureMouse(); + Refresh(); + return true; + } + + if (evt.LeftUp() && m_reset_overlay_pressed) { + const bool activate = over; + m_reset_overlay_pressed = false; + if (HasCapture()) + ReleaseMouse(); + if (activate) + reset_view(); + else + Refresh(); + return true; + } + + return over; +} + +void TexturePreviewCanvas::update_bounding_box() +{ + if (m_vertices.empty()) return; + std::array mn = m_vertices[0], mx = m_vertices[0]; + for (const auto& v : m_vertices) { + for (int i = 0; i < 3; ++i) { + mn[i] = std::min(mn[i], v[i]); + mx[i] = std::max(mx[i], v[i]); + } + } + m_center = { (mn[0]+mx[0])/2, (mn[1]+mx[1])/2, (mn[2]+mx[2])/2 }; + float dx = mx[0]-mn[0], dy = mx[1]-mn[1], dz = mx[2]-mn[2]; + m_radius = std::sqrt(dx*dx + dy*dy + dz*dz) / 2.0f; + if (m_radius < 1e-6f) m_radius = 1.0f; +} + +void TexturePreviewCanvas::ensure_gl_ready() +{ + if (m_gl_initialized) return; + + // BBS loads the GL entry points here with GLEW; Orca loads them centrally in + // OpenGLManager, so only check that this has already happened (glad leaves unresolved + // entry points null) and drain any stale error state. + if (glGetString == nullptr) { + BOOST_LOG_TRIVIAL(error) << "TexturePreviewCanvas: OpenGL functions are not loaded yet"; + return; + } + while (glGetError() != GL_NO_ERROR) {} + + m_gl_initialized = true; + + glEnable(GL_DEPTH_TEST); + glEnable(GL_LIGHTING); + glEnable(GL_LIGHT0); + glEnable(GL_COLOR_MATERIAL); + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); + + GLfloat light_pos[] = { 0.5f, 1.0f, 1.0f, 0.0f }; + GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f }; + GLfloat light_diffuse[] = { 0.8f, 0.8f, 0.8f, 1.0f }; + glLightfv(GL_LIGHT0, GL_POSITION, light_pos); + glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); + glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); +} + +void TexturePreviewCanvas::on_paint(wxPaintEvent&) +{ + wxPaintDC dc(this); + if (!m_context) return; + SetCurrent(*m_context); + ensure_gl_ready(); + render(); + SwapBuffers(); +} + +void TexturePreviewCanvas::on_size(wxSizeEvent&) +{ + Refresh(); +} + +void TexturePreviewCanvas::on_mouse(wxMouseEvent& evt) +{ + if (handle_reset_overlay_mouse(evt)) + return; + + if (evt.LeftDown()) { + m_drag_mode = DragMode::Rotate; + m_last_mouse_pos = evt.GetPosition(); + if (!HasCapture()) CaptureMouse(); + } + else if (evt.LeftUp()) { + if (m_drag_mode == DragMode::Rotate) { + m_drag_mode = DragMode::None; + if (HasCapture()) ReleaseMouse(); + } + } + else if (evt.RightDown()) { + m_drag_mode = DragMode::Pan; + m_last_mouse_pos = evt.GetPosition(); + if (!HasCapture()) CaptureMouse(); + } + else if (evt.MiddleDown()) { + m_drag_mode = DragMode::Pan; + m_last_mouse_pos = evt.GetPosition(); + if (!HasCapture()) CaptureMouse(); + } + else if (evt.RightUp() || evt.MiddleUp()) { + if (m_drag_mode == DragMode::Pan) { + m_drag_mode = DragMode::None; + if (HasCapture()) ReleaseMouse(); + } + } + else if (evt.Dragging() && m_drag_mode != DragMode::None) { + wxPoint pos = evt.GetPosition(); + float dx = (float)(pos.x - m_last_mouse_pos.x); + float dy = (float)(pos.y - m_last_mouse_pos.y); + + if (m_drag_mode == DragMode::Rotate) { + m_rot_y += dx * 0.5f; + m_rot_x += dy * 0.5f; + m_rot_x = std::max(-89.0f, std::min(89.0f, m_rot_x)); + } else if (m_drag_mode == DragMode::Pan) { + wxSize sz = GetClientSize(); + if (sz.x > 0) + m_pan_x += dx / (float)sz.x * m_radius * 2.0f / m_zoom; + if (sz.y > 0) + m_pan_y -= dy / (float)sz.y * m_radius * 2.0f / m_zoom; + } + + m_last_mouse_pos = pos; + Refresh(); + } + else if (evt.GetWheelRotation() != 0) { + float delta = evt.GetWheelRotation() > 0 ? 1.1f : 0.9f; + m_zoom *= delta; + m_zoom = std::max(0.1f, std::min(20.0f, m_zoom)); + Refresh(); + } +} + +void TexturePreviewCanvas::render() +{ + wxSize sz = GetClientSize(); + if (sz.x <= 0 || sz.y <= 0) return; + + wxSize viewport_sz = gl_viewport_size(this, sz); + glViewport(0, 0, viewport_sz.x, viewport_sz.y); + // Same palette key as the preview container, so canvas and frame cannot drift apart. + const wxColour clear_clr = StateColor::darkModeColorFor(wxColour("#EEEEEE")); + glClearColor(clear_clr.Red() / 255.f, clear_clr.Green() / 255.f, clear_clr.Blue() / 255.f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + float aspect = (float)viewport_sz.x / (float)viewport_sz.y; + float dist = m_radius * 3.0f / m_zoom; + float near_plane = dist * 0.01f; + float far_plane = dist * 10.0f; + float fov_rad = 45.0f * static_cast(M_PI) / 180.0f; + float f = 1.0f / std::tan(fov_rad / 2.0f); + float proj[16] = {}; + proj[0] = f / aspect; + proj[5] = f; + proj[10] = (far_plane + near_plane) / (near_plane - far_plane); + proj[11] = -1.0f; + proj[14] = (2.0f * far_plane * near_plane) / (near_plane - far_plane); + glMultMatrixf(proj); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslatef(0.0f, 0.0f, -dist); + glTranslatef(m_pan_x, m_pan_y, 0.0f); + glRotatef(m_rot_x, 1.0f, 0.0f, 0.0f); + glRotatef(m_rot_y, 0.0f, 1.0f, 0.0f); + glTranslatef(-m_center[0], -m_center[1], -m_center[2]); + + render_mesh(); + render_reset_overlay(sz, viewport_sz); +} + +void TexturePreviewCanvas::render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size) +{ + if (logical_size.x <= 0 || logical_size.y <= 0 || viewport_size.x <= 0 || viewport_size.y <= 0) + return; + + upload_reset_icon_textures(); + + const unsigned int tex_id = is_dark() + ? (m_reset_overlay_hovered ? m_reset_icon_dark_hover_tex : m_reset_icon_dark_tex) + : (m_reset_overlay_hovered ? m_reset_icon_hover_tex : m_reset_icon_tex); + if (!tex_id) + return; + + wxRect rc = reset_overlay_rect(); + const float sx = (float)viewport_size.x / (float)logical_size.x; + const float sy = (float)viewport_size.y / (float)logical_size.y; + const float x0 = rc.GetLeft() * sx; + const float y0 = rc.GetTop() * sy; + const float x1 = (rc.GetLeft() + rc.GetWidth()) * sx; + const float y1 = (rc.GetTop() + rc.GetHeight()) * sy; + const float alpha = m_reset_overlay_hovered ? 1.0f : 0.78f; + + glPushAttrib(GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_TEXTURE_BIT | GL_DEPTH_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, tex_id); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(0.0, viewport_size.x, viewport_size.y, 0.0, -1.0, 1.0); + + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + glColor4f(1.0f, 1.0f, 1.0f, alpha); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2f(x0, y0); + glTexCoord2f(1.0f, 0.0f); glVertex2f(x1, y0); + glTexCoord2f(1.0f, 1.0f); glVertex2f(x1, y1); + glTexCoord2f(0.0f, 1.0f); glVertex2f(x0, y1); + glEnd(); + + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + + glBindTexture(GL_TEXTURE_2D, 0); + glPopAttrib(); +} + +void TexturePreviewCanvas::render_textured_original() +{ + if (m_vertices.empty() || m_indices.empty()) return; + if (m_face_uvs.empty() || m_face_tex_ids.empty()) return; + if (m_face_uvs.size() != m_indices.size()) return; + + upload_textures(); + + const bool has_smooth = (m_vertex_normals.size() == m_vertices.size()); + + // Group faces by texture id for batch rendering + std::map> tex_groups; + for (size_t fi = 0; fi < m_indices.size(); ++fi) { + int tid = (fi < m_face_tex_ids.size()) ? m_face_tex_ids[fi] : -1; + tex_groups[tid].push_back(fi); + } + + glEnable(GL_LIGHTING); + glColor3f(1.0f, 1.0f, 1.0f); + + for (const auto& [tid, face_list] : tex_groups) { + bool tex_bound = false; + if (tid >= 0 && tid < (int)m_gl_tex_ids.size() && m_gl_tex_ids[tid] != 0) { + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, m_gl_tex_ids[tid]); + tex_bound = true; + } else { + glDisable(GL_TEXTURE_2D); + } + + glBegin(GL_TRIANGLES); + for (size_t fi : face_list) { + const auto& face = m_indices[fi]; + const auto& uvs = m_face_uvs[fi]; + + if (!tex_bound) { + if (fi < m_original_face_colors_rgb.size()) + glColor3fv(m_original_face_colors_rgb[fi].data()); + else + glColor3f(0.7f, 0.7f, 0.7f); + } + + for (int vi = 0; vi < 3; ++vi) { + int idx = face[vi]; + if (idx < 0 || idx >= (int)m_vertices.size()) continue; + + if (has_smooth) { + glNormal3fv(m_vertex_normals[idx].data()); + } else if (vi == 0) { + const auto& v0 = m_vertices[face[0]]; + const auto& v1 = m_vertices[face[1]]; + const auto& v2 = m_vertices[face[2]]; + float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]); + float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]); + float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]); + float len = std::sqrt(nx*nx + ny*ny + nz*nz); + if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; } + glNormal3f(nx, ny, nz); + } + + if (tex_bound) + glTexCoord2fv(uvs[vi].data()); + glVertex3fv(m_vertices[idx].data()); + } + } + glEnd(); + } + + glDisable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void TexturePreviewCanvas::render_mesh() +{ + if (m_vertices.empty() || m_indices.empty()) return; + + // Original mode with texture data: use proper texture mapping + if (m_mode == RenderMode::Original && !m_face_uvs.empty()) { + render_textured_original(); + return; + } + + // For Multi-Color / FilamentMap, use the painted (remeshed) geometry if available; + // the face color arrays match the painted mesh, not the original mesh. + const bool use_painted = (m_mode != RenderMode::Original) + && !m_painted_vertices.empty() + && !m_painted_indices.empty(); + + const auto& verts = use_painted ? m_painted_vertices : m_vertices; + const auto& faces = use_painted ? m_painted_indices : m_indices; + + const std::vector>* colors_ptr = nullptr; + if (m_mode == RenderMode::Original && !m_original_face_colors_rgb.empty() + && m_original_face_colors_rgb.size() == m_indices.size()) { + colors_ptr = &m_original_face_colors_rgb; + } else if (m_mode == RenderMode::FilamentMap && !m_filament_colors_rgb.empty() + && m_filament_colors_rgb.size() == faces.size()) { + colors_ptr = &m_filament_colors_rgb; + } else if (!m_face_colors_rgb.empty() && m_face_colors_rgb.size() == faces.size()) { + colors_ptr = &m_face_colors_rgb; + } + + // Use smooth normals for the original mesh when available + const bool has_smooth = !use_painted + && (m_vertex_normals.size() == m_vertices.size()); + + glDisable(GL_TEXTURE_2D); + glEnable(GL_LIGHTING); + + glBegin(GL_TRIANGLES); + for (size_t fi = 0; fi < faces.size(); ++fi) { + if (colors_ptr) + glColor3fv((*colors_ptr)[fi].data()); + else + glColor3f(0.7f, 0.7f, 0.7f); + + const auto& face = faces[fi]; + for (int vi = 0; vi < 3; ++vi) { + int idx = face[vi]; + if (idx < 0 || idx >= (int)verts.size()) continue; + + if (has_smooth && idx < (int)m_vertex_normals.size()) { + glNormal3fv(m_vertex_normals[idx].data()); + } else if (vi == 0) { + const auto& v0 = verts[face[0]]; + const auto& v1 = verts[face[1]]; + const auto& v2 = verts[face[2]]; + float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]); + float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]); + float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]); + float len = std::sqrt(nx*nx + ny*ny + nz*nz); + if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; } + glNormal3f(nx, ny, nz); + } + + glVertex3fv(verts[idx].data()); + } + } + glEnd(); +} + + +// ============================================================ +// TextureImportDialog +// ============================================================ + +wxBEGIN_EVENT_TABLE(TextureImportDialog, DPIDialog) + EVT_BUTTON(TextureImportDialog::ID_COLOR_4, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_COLOR_8, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_COLOR_16, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_COLOR_AUTO, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_BTN_APPLY, TextureImportDialog::on_apply_clicked) + EVT_BUTTON(TextureImportDialog::ID_BTN_SKIP, TextureImportDialog::on_skip_clicked) + EVT_BUTTON(wxID_OK, TextureImportDialog::on_ok_clicked) +wxEND_EVENT_TABLE() + +TextureImportDialog::TextureImportDialog( + wxWindow* parent, + const Slic3r::TexturedMesh& textured_mesh, + const std::vector& filament_entries, + std::function initial_cancel_callback, + std::function initial_progress_callback) + : DPIDialog(parent, wxID_ANY, _L("Import Model"), + wxDefaultPosition, wxDefaultSize, + (wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) & ~(wxMINIMIZE_BOX | wxMAXIMIZE_BOX)) + , m_textured_mesh(textured_mesh) + , m_filament_entries(filament_entries) + , m_initial_cancel_callback(std::move(initial_cancel_callback)) + , m_initial_progress_callback(std::move(initial_progress_callback)) +{ + SetSize(wxSize(FromDIP(960), FromDIP(640))); + + m_filament_colors_rgba.reserve(m_filament_entries.size()); + m_filament_color_strs.reserve(m_filament_entries.size()); + m_filament_names.reserve(m_filament_entries.size()); + for (size_t i = 0; i < m_filament_entries.size(); ++i) { + auto& entry = m_filament_entries[i]; + entry.dialog_index = (int)i; + entry.color_hex = texture_normalize_color_hex(entry.color_hex); + if (entry.name.empty()) + entry.name = "Filament " + std::to_string(i + 1); + m_filament_color_strs.push_back(entry.color_hex); + m_filament_names.push_back(entry.name); + m_filament_colors_rgba.push_back(parse_color_string(entry.color_hex)); + } + + m_existing_filament_count = m_filament_colors_rgba.size(); + m_default_virtual_filament_preset_name = resolve_default_virtual_filament_preset_name(); + + Bind(EVT_TEXTURE_COMPUTE_DONE, &TextureImportDialog::on_computation_complete, this); + Bind(EVT_TEXTURE_COMPUTE_PROGRESS, &TextureImportDialog::on_computation_progress, this); + Bind(EVT_TEXTURE_COMPUTE_ERROR, &TextureImportDialog::on_computation_error, this); + Bind(EVT_TEXTURE_MESH_REPAIR_DECISION, &TextureImportDialog::on_mesh_repair_decision_required, this); + + build_ui(); + SetMinSize(wxSize(FromDIP(800), FromDIP(500))); + CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); + + m_preview_canvas->set_mesh_data(m_textured_mesh.vertices, m_textured_mesh.indices); + + // Pre-computed face colors (OBJ vertex colors / MTL face colors): + // use them directly as the Original preview, skip texture decode. + if (!m_textured_mesh.precomputed_face_colors.empty()) { + m_preview_canvas->set_original_face_colors(m_textured_mesh.precomputed_face_colors); + } else if (!m_textured_mesh.textures.empty()) { + std::vector> tex_pixels_rgb; + std::vector tex_widths, tex_heights; + tex_pixels_rgb.reserve(m_textured_mesh.textures.size()); + tex_widths.reserve(m_textured_mesh.textures.size()); + tex_heights.reserve(m_textured_mesh.textures.size()); + + for (const auto& ti : m_textured_mesh.textures) { + std::vector bgr_pixels; + int w = 0, h = 0; + if (Slic3r::decode_texture_to_pixels(ti, bgr_pixels, w, h) && !bgr_pixels.empty()) { + // Convert BGR to RGB for OpenGL + for (size_t p = 0; p < bgr_pixels.size(); p += 3) + std::swap(bgr_pixels[p], bgr_pixels[p + 2]); + tex_pixels_rgb.push_back(std::move(bgr_pixels)); + } else { + tex_pixels_rgb.push_back({}); + } + tex_widths.push_back(w); + tex_heights.push_back(h); + } + + const size_t nf = m_textured_mesh.indices.size(); + const bool has_mapping = !m_textured_mesh.material_texture_map.empty(); + + // Build per-face UV array + std::vector, 3>> face_uvs(nf); + for (size_t fi = 0; fi < nf; ++fi) { + if (m_textured_mesh.has_face_uvs()) { + const auto& ui = m_textured_mesh.uv_indices[fi]; + for (int vi = 0; vi < 3; ++vi) { + int idx = ui[vi]; + if (idx >= 0 && static_cast(idx) < m_textured_mesh.uv_coords.size()) + face_uvs[fi][vi] = m_textured_mesh.uv_coords[idx]; + else + face_uvs[fi][vi] = {0.f, 0.f}; + } + } else if (!m_textured_mesh.uvs.empty()) { + const auto& face = m_textured_mesh.indices[fi]; + for (int vi = 0; vi < 3; ++vi) { + int idx = face[vi]; + if (idx >= 0 && static_cast(idx) < m_textured_mesh.uvs.size()) + face_uvs[fi][vi] = m_textured_mesh.uvs[idx]; + else + face_uvs[fi][vi] = {0.f, 0.f}; + } + } + } + + // Build per-face texture index + std::vector face_tex_ids(nf, 0); + for (size_t fi = 0; fi < nf; ++fi) { + int mat_idx = (fi < m_textured_mesh.material_ids.size()) + ? m_textured_mesh.material_ids[fi] : -1; + if (has_mapping && mat_idx >= 0 + && static_cast(mat_idx) < m_textured_mesh.material_texture_map.size()) + face_tex_ids[fi] = m_textured_mesh.material_texture_map[mat_idx]; + else if (!tex_pixels_rgb.empty()) + face_tex_ids[fi] = 0; + else + face_tex_ids[fi] = -1; + } + + m_preview_canvas->set_texture_render_data( + tex_pixels_rgb, tex_widths, tex_heights, face_uvs, face_tex_ids); + + // Still sample per-face colors as fallback + std::vector> orig_colors; + if (Slic3r::sample_original_face_colors(m_textured_mesh, orig_colors)) + m_preview_canvas->set_original_face_colors(orig_colors); + } + + set_state(TextureImportState::Idle); +} + +TextureImportDialog::~TextureImportDialog() +{ + dismiss_auto_mix_popup(); + dismiss_filament_popup(); + m_cancel_flag = true; + if (m_worker && m_worker->joinable()) + m_worker->join(); +} + +int TextureImportDialog::ShowModal() +{ + if (m_state == TextureImportState::Idle && m_painted.face_colors.empty()) { + start_computation(true, true); + + while (m_initial_computation_pending) { + if (auto* event_loop = wxEventLoopBase::GetActive()) + event_loop->Yield(); + else + wxYield(); + if (m_progress_dlg && m_progress_dlg->WasCancelled()) + m_cancel_flag = true; + if (m_initial_cancel_callback && m_initial_cancel_callback()) + m_cancel_flag = true; + wxMilliSleep(10); + } + + if (m_worker && m_worker->joinable()) + m_worker->join(); + m_worker.reset(); + + if (m_initial_computation_cancelled || m_initial_computation_failed) + return wxID_CANCEL; + } + + ScopedInteractiveBusyCursorSuspender busy_cursor_suspender; + return DPIDialog::ShowModal(); +} + +void TextureImportDialog::build_ui() +{ + const wxColour dialog_bg = StateColor::darkModeColorFor(*wxWHITE); + SetBackgroundColour(dialog_bg); + SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + + wxBoxSizer* root_sizer = new wxBoxSizer(wxVERTICAL); + + auto line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1)); + line_top->SetBackgroundColour(texture_import_separator_colour()); + root_sizer->Add(line_top, 0, wxEXPAND); + + wxBoxSizer* main_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxBoxSizer* left_sizer = new wxBoxSizer(wxVERTICAL); + build_preview_panel(this, left_sizer); + main_sizer->Add(left_sizer, 3, wxEXPAND | wxALL, FromDIP(8)); + + wxBoxSizer* right_sizer = new wxBoxSizer(wxVERTICAL); + build_params_panel(this, right_sizer); + build_mapping_panel(this, right_sizer); + build_bottom_buttons(right_sizer); + main_sizer->Add(right_sizer, 2, wxEXPAND | wxALL, FromDIP(8)); + + root_sizer->Add(main_sizer, 1, wxEXPAND); + + SetSizer(root_sizer); + Layout(); + Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + +#ifdef __WXMSW__ + wxPanel* size_grip_cover = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + size_grip_cover->SetBackgroundColour(dialog_bg); + size_grip_cover->SetBackgroundStyle(wxBG_STYLE_COLOUR); + + auto update_size_grip_cover = [this, size_grip_cover]() { + const int cover_size = FromDIP(20); + wxSize client_size = GetClientSize(); + size_grip_cover->SetSize(client_size.x - cover_size, client_size.y - cover_size, cover_size, cover_size); + size_grip_cover->Raise(); + }; + update_size_grip_cover(); + + Bind(wxEVT_SIZE, [update_size_grip_cover](wxSizeEvent& e) { + e.Skip(); + update_size_grip_cover(); + }); +#endif +} + +void TextureImportDialog::build_preview_panel(wxWindow* parent, wxSizer* sizer) +{ + wxColour preview_bg = StateColor::darkModeColorFor(wxColour("#EEEEEE")); + wxColour preview_bd = texture_import_separator_colour(); + + wxPanel* preview_container = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + preview_container->SetBackgroundColour(preview_bg); + preview_container->SetBackgroundStyle(wxBG_STYLE_PAINT); + preview_container->Bind(wxEVT_PAINT, [preview_bg, preview_bd](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + dc.SetBrush(wxBrush(preview_bg)); + dc.SetPen(wxPen(preview_bd, 1)); + dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, 4); + }); + + wxBoxSizer* container_sizer = new wxBoxSizer(wxVERTICAL); + + wxGLAttributes canvas_attrs; + canvas_attrs.PlatformDefaults().RGBA().DoubleBuffer().Depth(24).EndList(); + m_preview_canvas = new TexturePreviewCanvas(preview_container, canvas_attrs); + container_sizer->Add(m_preview_canvas, 1, wxEXPAND | wxALL, FromDIP(1)); + + preview_container->SetSizer(container_sizer); + sizer->Add(preview_container, 1, wxEXPAND); + + m_tab_panel = new wxPanel(preview_container, wxID_ANY); + m_tab_panel->SetBackgroundColour(preview_bg); + + m_btn_view_original = new Button(m_tab_panel, _L("Original")); + m_btn_view_original->SetId(ID_VIEW_ORIGINAL); + m_btn_view_multicolor = new Button(m_tab_panel, _L("Multi-Color")); + m_btn_view_multicolor->SetId(ID_VIEW_MULTICOLOR); + + const int view_button_height = FromDIP(27); + m_btn_view_original->SetCornerRadius(view_button_height / 2); + m_btn_view_original->SetMinSize(wxSize(FromDIP(57), view_button_height)); + m_btn_view_original->SetFont(m_btn_view_original->GetFont().Bold()); + m_btn_view_original->SetToolTip(_L("Your input texture model")); + m_btn_view_multicolor->SetCornerRadius(view_button_height / 2); + m_btn_view_multicolor->SetMinSize(wxSize(FromDIP(57), view_button_height)); + m_btn_view_multicolor->SetFont(m_btn_view_multicolor->GetFont().Bold()); + m_btn_view_multicolor->SetToolTip(_L("Processed multi-color model")); + + wxBoxSizer* tab_sizer = new wxBoxSizer(wxHORIZONTAL); + tab_sizer->Add(m_btn_view_original, 0, wxRIGHT, FromDIP(2)); + tab_sizer->Add(m_btn_view_multicolor, 0); + m_tab_panel->SetSizer(tab_sizer); + m_tab_panel->Fit(); + + m_btn_view_multicolor->Hide(); + + auto preview_original = [this](wxMouseEvent& e) { + if (m_preview_canvas) { + m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::Original); + highlight_view_button(0); + } + e.Skip(); + }; + auto preview_multicolor = [this](wxMouseEvent& e) { + if (m_preview_canvas) { + m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::MultiColor); + highlight_view_button(1); + } + e.Skip(); + }; + auto restore_filament_if_outside = [this](wxMouseEvent& e) { + if (m_preview_canvas && m_tab_panel) { + wxWindow* event_window = wxDynamicCast(e.GetEventObject(), wxWindow); + wxPoint screen_pos = event_window ? event_window->ClientToScreen(e.GetPosition()) : wxGetMousePosition(); + wxPoint panel_pos = m_tab_panel->ScreenToClient(screen_pos); + if (!m_tab_panel->GetClientRect().Contains(panel_pos)) { + const bool mapping_ready = (m_state == TextureImportState::Ready); + m_preview_canvas->set_render_mode(mapping_ready ? TexturePreviewCanvas::RenderMode::FilamentMap : + TexturePreviewCanvas::RenderMode::Original); + highlight_view_button(-1); + } + } + e.Skip(); + }; + + m_btn_view_original->Bind(wxEVT_ENTER_WINDOW, preview_original); + m_btn_view_multicolor->Bind(wxEVT_ENTER_WINDOW, preview_multicolor); + m_btn_view_original->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside); + m_btn_view_multicolor->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside); + m_tab_panel->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside); + + auto update_preview_overlay_buttons = [this]() { + if (m_tab_panel) { + m_tab_panel->Fit(); + m_tab_panel->SetPosition(wxPoint(FromDIP(8), FromDIP(8))); + m_tab_panel->Raise(); + } + }; + + preview_container->Bind(wxEVT_SIZE, [update_preview_overlay_buttons](wxSizeEvent& e) { + e.Skip(); + update_preview_overlay_buttons(); + }); + update_preview_overlay_buttons(); + + highlight_view_button(-1); +} + +void TextureImportDialog::build_params_panel(wxWindow* parent, wxSizer* sizer) +{ + wxColour label_fg = StateColor::darkModeColorFor(wxColour("#323A3D")); + + wxBoxSizer* color_header_sizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticText* lbl_colors = new wxStaticText(parent, wxID_ANY, _L("Color Count")); + lbl_colors->SetForegroundColour(label_fg); + lbl_colors->SetFont(lbl_colors->GetFont().Bold()); + color_header_sizer->Add(lbl_colors, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + m_btn_color_4 = new Button(parent, "4"); + m_btn_color_4->SetId(ID_COLOR_4); + m_btn_color_8 = new Button(parent, "8"); + m_btn_color_8->SetId(ID_COLOR_8); + m_btn_color_16 = new Button(parent, "16"); + m_btn_color_16->SetId(ID_COLOR_16); + m_btn_color_auto = new Button(parent, _L("Auto")); + m_btn_color_auto->SetId(ID_COLOR_AUTO); + + { + StateColor preset_bg( + std::pair(wxColour(0, 137, 123), StateColor::Pressed | StateColor::Checked), + std::pair(wxColour(38, 166, 154), StateColor::Hovered | StateColor::Checked), + std::pair(wxColour(0, 150, 136), StateColor::Checked), + std::pair(wxColour("#CECECE"), StateColor::Pressed), + std::pair(wxColour("#EEEEEE"), StateColor::Hovered), + std::pair(*wxWHITE, StateColor::Normal)); + StateColor preset_bd( + std::pair(wxColour(0, 150, 136), StateColor::Checked), + std::pair(wxColour("#CECECE"), StateColor::Normal)); + StateColor preset_text( + std::pair(wxColour("#FFFFFE"), StateColor::Checked), + std::pair(wxColour("#323A3D"), StateColor::Normal)); + + for (auto* btn : {m_btn_color_4, m_btn_color_8, m_btn_color_16}) { + btn->SetCornerRadius(FromDIP(12)); + btn->SetMinSize(wxSize(FromDIP(28), FromDIP(28))); + btn->SetBackgroundColor(preset_bg); + btn->SetBorderColor(preset_bd); + btn->SetTextColor(preset_text); + } + } + + update_color_count_preset_buttons(); + + color_header_sizer->Add(m_btn_color_4, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + color_header_sizer->Add(m_btn_color_8, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + color_header_sizer->Add(m_btn_color_16, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(color_header_sizer, 0, wxBOTTOM, FromDIP(4)); + + wxBoxSizer* color_slider_sizer = new wxBoxSizer(wxHORIZONTAL); + m_color_slider = new AccentSlider(parent, m_param_color_count, 1, (int)max_filament_count()); + m_color_spin = new SpinInput(parent, wxString::Format("%d", m_param_color_count), + wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(60), FromDIP(28)), + wxTE_PROCESS_ENTER, 1, (int)max_filament_count(), m_param_color_count); + + m_color_slider->Bind(wxEVT_SLIDER, &TextureImportDialog::on_color_slider_changed, this); + m_color_spin->Bind(wxEVT_SPINCTRL, &TextureImportDialog::on_color_spin_changed, this); + m_color_spin->Bind(EVT_SPINCTRL_TEXT, &TextureImportDialog::on_color_spin_text_changed, this); + + color_slider_sizer->Add(m_color_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + color_slider_sizer->Add(m_color_spin, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(color_slider_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + wxStaticText* lbl_smooth = new wxStaticText(parent, wxID_ANY, _L("Smooth Level")); + lbl_smooth->SetForegroundColour(label_fg); + lbl_smooth->SetFont(lbl_smooth->GetFont().Bold()); + sizer->Add(lbl_smooth, 0, wxBOTTOM, FromDIP(4)); + + wxBoxSizer* smooth_sizer = new wxBoxSizer(wxHORIZONTAL); + m_smooth_slider = new AccentSlider(parent, m_param_smooth, 0, 10); + m_smooth_spin = new SpinInput(parent, wxString::Format("%d", m_param_smooth), + wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(60), FromDIP(28)), + wxTE_PROCESS_ENTER, 0, 10, m_param_smooth); + + m_smooth_slider->Bind(wxEVT_SLIDER, &TextureImportDialog::on_smooth_slider_changed, this); + m_smooth_spin->Bind(wxEVT_SPINCTRL, &TextureImportDialog::on_smooth_spin_changed, this); + m_smooth_spin->Bind(EVT_SPINCTRL_TEXT, &TextureImportDialog::on_smooth_spin_text_changed, this); + + smooth_sizer->Add(m_smooth_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + smooth_sizer->Add(m_smooth_spin, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(smooth_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + m_btn_apply = new Button(parent, _L("Apply")); + m_btn_apply->SetId(ID_BTN_APPLY); + + { + StateColor btn_bg_white( + std::pair(wxColour("#CECECE"), StateColor::Pressed), + std::pair(wxColour("#EEEEEE"), StateColor::Hovered), + std::pair(*wxWHITE, StateColor::Normal)); + const wxColour btn_bd_accent = wxColour(0, 150, 136); + const wxColour btn_text_accent = wxColour(0, 150, 136); + + m_btn_color_auto->SetCornerRadius(FromDIP(12)); + m_btn_color_auto->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + m_btn_color_auto->SetBackgroundColor(btn_bg_white); + m_btn_color_auto->SetBorderColor(btn_bd_accent); + m_btn_color_auto->SetTextColor(btn_text_accent); + + m_btn_apply->SetCornerRadius(FromDIP(12)); + m_btn_apply->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + m_btn_apply->SetBackgroundColor(btn_bg_white); + m_btn_apply->SetBorderColor(btn_bd_accent); + m_btn_apply->SetTextColor(btn_text_accent); + } + + // Defer attaching the Auto/Apply tooltips until the dialog has actually + // been shown. On macOS, AppKit creates NSTrackingArea and dispatches a + // synthetic mouseEntered: as soon as the window first becomes visible, + // which would otherwise pop the native tooltip without the user actually + // hovering when the cursor happens to land on these buttons as the dialog + // appears. + Bind(wxEVT_SHOW, [this](wxShowEvent& e) { + e.Skip(); + if (!e.IsShown() || m_initial_tooltips_set) + return; + m_initial_tooltips_set = true; + CallAfter([this]() { + if (m_btn_color_auto) + m_btn_color_auto->SetToolTip(_L("Automatically determine the optimal color count only and recompute filament mapping")); + if (m_btn_apply) + m_btn_apply->SetToolTip(_L("Convert texture to painting using the specified color count and smooth level")); + }); + }); + + wxBoxSizer* apply_sizer = new wxBoxSizer(wxHORIZONTAL); + apply_sizer->Add(m_btn_color_auto, 0, wxRIGHT, FromDIP(4)); + apply_sizer->Add(m_btn_apply, 0); + sizer->Add(apply_sizer, 0, wxALIGN_RIGHT | wxBOTTOM, FromDIP(8)); + + m_hint_label = new wxStaticText(parent, wxID_ANY, + _L("Reminder: parameters changed, click Apply to take effect")); + m_hint_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00"))); + m_hint_label->SetFont(texture_import_section_title_font(parent)); + m_hint_label->Hide(); + sizer->Add(m_hint_label, 0, wxBOTTOM, FromDIP(4)); + + auto* mapping_separator = new StaticLine(parent); + mapping_separator->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + sizer->Add(mapping_separator, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); +} + +void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer) +{ + wxColour secondary_fg = StateColor::darkModeColorFor(wxColour("#6B6B6B")); + + wxBoxSizer* header_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxStaticText* lbl_mapping = new wxStaticText(parent, wxID_ANY, _L("Filament Mapping")); + lbl_mapping->SetForegroundColour(secondary_fg); + lbl_mapping->SetFont(texture_import_section_title_font(parent)); + m_auto_mix_font_point_size = lbl_mapping->GetFont().GetPointSize(); + header_sizer->Add(lbl_mapping, 0, wxALIGN_CENTER_VERTICAL); + + m_btn_mix_reset = new Button(parent, "", "revert_btn", wxBORDER_NONE, 16); + m_btn_mix_reset->SetCanFocus(false); + m_btn_mix_reset->SetPaddingSize(wxSize(FromDIP(2), FromDIP(2))); + { + StateColor reset_bg( + std::pair(wxColour("#F4F4F4"), StateColor::Pressed), + std::pair(wxColour("#F8F8F8"), StateColor::Hovered), + std::pair(*wxWHITE, StateColor::Normal)); + m_btn_mix_reset->SetBackgroundColor(reset_bg); + m_btn_mix_reset->SetBorderColor(StateColor()); + } + m_btn_mix_reset->SetToolTip(_L("Reset filament mapping to the state before one-click mixing")); + m_btn_mix_reset->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { + reset_auto_mix(); + evt.Skip(); + }); + m_btn_mix_reset->Hide(); + header_sizer->Add(m_btn_mix_reset, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + + header_sizer->AddStretchSpacer(); + + m_btn_auto_mix = new Button(parent, auto_mix_mode_label(m_auto_mix_mode)); + { + wxFont btn_font = m_btn_auto_mix->GetFont(); + btn_font.SetPointSize(m_auto_mix_font_point_size); + m_btn_auto_mix->SetFont(btn_font); + } + m_btn_auto_mix->SetCornerRadius(FromDIP(14)); + m_btn_auto_mix->SetMinSize(wxSize(FromDIP(178), FromDIP(28))); + { + StateColor btn_bg( + std::pair(wxColour("#F4F4F4"), StateColor::Pressed), + std::pair(wxColour("#F8F8F8"), StateColor::Hovered), + std::pair(*wxWHITE, StateColor::Normal)); + const wxColour btn_bd = wxColour("#CECECE"); + const wxColour btn_text = texture_import_gray9000(); + m_btn_auto_mix->SetBackgroundColor(btn_bg); + m_btn_auto_mix->SetBorderColor(btn_bd); + m_btn_auto_mix->SetTextColor(btn_text); + } + m_btn_auto_mix->SetToolTip(_L("Choose the one-click auto-mix mode for texture color import")); + m_btn_auto_mix->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { + show_auto_mix_popup(); + evt.Skip(); + }); + m_btn_auto_mix->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& evt) { + show_auto_mix_popup(); + evt.Skip(); + }); + header_sizer->Add(m_btn_auto_mix, 0, wxALIGN_CENTER_VERTICAL); + + sizer->Add(header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + wxBoxSizer* merge_sizer = new wxBoxSizer(wxHORIZONTAL); + m_auto_merge_cb = new wxCheckBox(parent, wxID_ANY, _L("Auto-merge same filament")); + m_auto_merge_cb->SetToolTip(_L("Automatically merge identical filaments into existing filaments in the project")); + m_auto_merge_cb->SetForegroundColour(secondary_fg); + m_auto_merge_cb->SetValue(true); + m_auto_merge_cb->Bind(wxEVT_CHECKBOX, &TextureImportDialog::on_auto_merge_toggled, this); + merge_sizer->Add(m_auto_merge_cb, 0, wxALIGN_CENTER_VERTICAL); + + sizer->Add(merge_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, + wxSize(-1, FromDIP(300))); + m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + + m_mapping_sizer = new wxBoxSizer(wxVERTICAL); + m_mapping_scroll->SetSizer(m_mapping_sizer); + + sizer->Add(m_mapping_scroll, 1, wxEXPAND | wxBOTTOM, FromDIP(8)); +} + +void TextureImportDialog::build_bottom_buttons(wxSizer* sizer) +{ + m_drop_warning_label = new wxStaticText(this, wxID_ANY, + wxString::Format( + _L("The project supports up to %d filaments. Extra filaments will be discarded."), + (int)max_filament_count())); + m_drop_warning_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00"))); + m_drop_warning_label->SetFont(texture_import_section_title_font(this)); + m_drop_warning_label->Hide(); + sizer->Add(m_drop_warning_label, 0, wxALIGN_LEFT | wxBOTTOM, FromDIP(4)); + + wxBoxSizer* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + m_btn_skip = new Button(this, _L("Skip Matching")); + m_btn_skip->SetId(ID_BTN_SKIP); + m_btn_skip->SetToolTip(_L("Skip filament mapping and import as a single-color model")); + m_btn_skip->SetCornerRadius(FromDIP(20)); + m_btn_skip->SetMinSize(wxSize(FromDIP(136), FromDIP(40))); + { + StateColor skip_bg( + std::pair(wxColour("#CECECE"), StateColor::Pressed), + std::pair(wxColour("#EEEEEE"), StateColor::Hovered), + std::pair(*wxWHITE, StateColor::Normal)); + const wxColour skip_bd = wxColour("#CECECE"); + const wxColour skip_text = wxColour("#6B6B6A"); + m_btn_skip->SetBackgroundColor(skip_bg); + m_btn_skip->SetBorderColor(skip_bd); + m_btn_skip->SetTextColor(skip_text); + } + + m_btn_ok = new Button(this, _L("Confirm")); + m_btn_ok->SetId(wxID_OK); + m_btn_ok->SetCornerRadius(FromDIP(20)); + m_btn_ok->SetMinSize(wxSize(FromDIP(156), FromDIP(40))); + apply_accent_button_colours(m_btn_ok); + + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(m_btn_skip, 0, wxRIGHT, FromDIP(16)); + btn_sizer->Add(m_btn_ok, 0); + + sizer->Add(btn_sizer, 0, wxEXPAND | wxTOP, FromDIP(8)); +} + +// ---- State machine ---- + +void TextureImportDialog::set_state(TextureImportState new_state) +{ + m_state = new_state; + update_ui_for_state(); +} + +void TextureImportDialog::update_ui_for_state() +{ + bool computing = (m_state == TextureImportState::Computing); + bool ready = (m_state == TextureImportState::Ready); + bool idle = (m_state == TextureImportState::Idle); + bool valid = has_valid_result(); + + m_color_slider->Enable(!computing); + m_color_spin->Enable(!computing); + m_smooth_slider->Enable(!computing); + m_smooth_spin->Enable(!computing); + m_btn_apply->Enable(!computing); + m_btn_color_4->Enable(!computing); + m_btn_color_8->Enable(!computing); + m_btn_color_16->Enable(!computing); + m_btn_color_auto->Enable(!computing); + if (m_btn_auto_mix) + m_btn_auto_mix->Enable(!computing); + if (m_btn_mix_reset) + m_btn_mix_reset->Enable(!computing); + if (computing) + dismiss_auto_mix_popup(); + + m_btn_ok->Enable(ready && valid); + m_btn_skip->Enable(ready || idle); + + m_auto_merge_cb->Enable(!computing); + + m_preview_canvas->set_computing_overlay(computing); + + if (ready && valid) + style_confirm_button(is_params_dirty()); + else if (m_hint_label) + m_hint_label->Hide(); + + m_btn_ok->Refresh(); + Layout(); +} + +// ---- Async computation ---- + +void TextureImportDialog::start_computation(bool auto_color, bool initial) +{ + cancel_computation(); + + m_cancel_flag = false; + m_current_computation_initial = initial; + m_current_computation_auto_color = auto_color; + if (initial) { + m_initial_computation_pending = true; + m_initial_computation_cancelled = false; + m_initial_computation_failed = false; + } + set_state(TextureImportState::Computing); + + bool silent_initial = initial && static_cast(m_initial_cancel_callback); + if (!silent_initial) { + m_progress_dlg = new ProgressDialog( + _L("Processing"), _L("Computing texture colors..."), + 100, initial ? GetParent() : this, wxPD_APP_MODAL | wxPD_CAN_ABORT | wxPD_AUTO_HIDE); + } + + Slic3r::TexturePaintingSettings settings; + settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count; + settings.smooth_weight = m_param_smooth / 10.0; + settings.mesh_repair_decision = m_mesh_repair_decision; + // BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows + // builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works + // on all three platforms, so use that instead. + settings.mesh_repair_callback = [](const indexed_triangle_set& mesh, + indexed_triangle_set& repaired_mesh, + std::function progress_callback, + std::function cancel_callback, + std::string* error_message) -> bool { + if (cancel_callback && cancel_callback()) + return false; + if (progress_callback) + progress_callback(_u8L("Repairing mesh").c_str(), 0); + + TriangleMesh tm(mesh); + if (!MeshBoolean::cgal::repair(tm, nullptr, error_message)) + return false; + + if (cancel_callback && cancel_callback()) + return false; + repaired_mesh = tm.its; + if (progress_callback) + progress_callback(_u8L("Repairing mesh").c_str(), 100); + return true; + }; + + Slic3r::TexturedMesh mesh_copy = m_textured_mesh; + wxEvtHandler* handler = this; + + m_worker = std::make_unique([this, settings, mesh_copy, handler]() { + Slic3r::PaintedMesh result; + + auto progress_cb = [handler](int percent, const char*) { + auto* evt = new wxCommandEvent(EVT_TEXTURE_COMPUTE_PROGRESS); + evt->SetInt(percent); + wxQueueEvent(handler, evt); + }; + + auto cancel_cb = [this]() -> bool { + return m_cancel_flag.load(); + }; + + auto worker_settings = settings; + bool mesh_repair_decision_required = false; + worker_settings.mesh_repair_decision_required = &mesh_repair_decision_required; + bool ok; + if (!mesh_copy.precomputed_face_colors.empty()) { + ok = Slic3r::face_colors_to_painting( + mesh_copy, result, worker_settings, progress_cb, cancel_cb); + } else { + ok = Slic3r::texture_to_painting(mesh_copy, result, worker_settings, progress_cb, cancel_cb); + } + + if (m_cancel_flag.load()) { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR)); + return; + } + + if (!ok && mesh_repair_decision_required) { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_MESH_REPAIR_DECISION)); + return; + } + + { + std::lock_guard lock(m_result_mutex); + m_pending_result = std::move(result); + } + + if (ok) { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_DONE)); + } else { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR)); + } + }); +} + +void TextureImportDialog::cancel_computation() +{ + m_cancel_flag = true; + if (m_worker && m_worker->joinable()) + m_worker->join(); + m_worker.reset(); + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + + if (m_current_computation_initial) { + m_initial_computation_cancelled = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + } +} + +void TextureImportDialog::on_computation_progress(wxCommandEvent& evt) +{ + if (m_progress_dlg) { + if (!m_progress_dlg->Update(evt.GetInt())) + m_cancel_flag = true; + } else if (m_current_computation_initial && m_initial_progress_callback) { + if (!m_initial_progress_callback(evt.GetInt())) + m_cancel_flag = true; + } +} + +void TextureImportDialog::on_computation_complete(wxCommandEvent&) +{ + bool initial = m_current_computation_initial; + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + + { + std::lock_guard lock(m_result_mutex); + m_painted = std::move(m_pending_result); + } + + int actual_colors = (int)m_painted.cluster_colors.size(); + if (actual_colors >= 2 && actual_colors <= (int)max_filament_count()) { + set_color_count_value(actual_colors, true); + } + + m_preview_canvas->set_painted_mesh_data(m_painted.vertices, m_painted.indices); + m_preview_canvas->set_face_colors(m_painted.face_colors); + + // A fresh texture computation replaces m_painted, so virtual filaments from + // the previous computation must not consume capacity when deciding whether + // this run drops extra colors. Rebuild virtual filaments from this result. + m_current_matches.clear(); + if (m_filament_colors_rgba.size() > m_existing_filament_count) + m_filament_colors_rgba.resize(m_existing_filament_count); + if (m_filament_color_strs.size() > m_existing_filament_count) + m_filament_color_strs.resize(m_existing_filament_count); + if (m_filament_names.size() > m_existing_filament_count) + m_filament_names.resize(m_existing_filament_count); + if (m_filament_entries.size() > m_existing_filament_count) + m_filament_entries.resize(m_existing_filament_count); + while (m_filament_entries.size() < m_existing_filament_count) { + TextureFilamentEntry entry; + entry.kind = TextureFilamentKind::ExistingPhysical; + entry.dialog_index = (int)m_filament_entries.size(); + entry.project_config_index = m_filament_entries.size(); + m_filament_entries.push_back(entry); + } + for (size_t i = 0; i < m_filament_entries.size(); ++i) { + m_filament_entries[i].dialog_index = (int)i; + m_filament_entries[i].color_hex = i < m_filament_color_strs.size() ? + texture_normalize_color_hex(m_filament_color_strs[i]) : "#808080"; + m_filament_entries[i].name = i < m_filament_names.size() ? + m_filament_names[i] : "Filament " + std::to_string(i + 1); + } + m_new_filament_colors.clear(); + m_new_filament_preset_names.clear(); + m_new_mixed_filaments.clear(); + + do_auto_match(); + compact_used_virtual_filaments(); + sort_current_matches_by_filament_index(); + update_filament_color_map(); + rebuild_mapping_rows(); + + m_applied_color_count = m_param_color_count; + m_applied_smooth = m_param_smooth; + + set_state(TextureImportState::Ready); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); + + m_btn_view_multicolor->Show(); + if (m_tab_panel) { + m_tab_panel->GetSizer()->Layout(); + m_tab_panel->Fit(); + m_tab_panel->SetPosition(wxPoint(FromDIP(8), FromDIP(8))); + } + GetSizer()->Layout(); + + m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::FilamentMap); + highlight_view_button(-1); + + if (initial) { + m_initial_computation_pending = false; + m_current_computation_initial = false; + } +} + +void TextureImportDialog::on_computation_error(wxCommandEvent&) +{ + bool initial = m_current_computation_initial; + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + + if (m_cancel_flag.load()) { + if (initial) { + m_initial_computation_cancelled = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + return; + } + if (has_valid_result()) { + if (m_applied_color_count >= 0) { + m_param_color_count = m_applied_color_count; + m_color_slider->SetValue(m_param_color_count); + m_color_spin->SetValue(m_param_color_count); + update_color_count_preset_buttons(); + } + if (m_applied_smooth >= 0) { + m_param_smooth = m_applied_smooth; + m_smooth_slider->SetValue(m_param_smooth); + m_smooth_spin->SetValue(m_param_smooth); + } + set_state(TextureImportState::Ready); + return; + } + set_state(TextureImportState::Idle); + return; + } + + if (initial) { + m_initial_computation_failed = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + m_fallback_to_geometry_only = true; + return; + } + + set_state(TextureImportState::Error); + Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, + _L("Computation failed. Please adjust parameters and retry."), + _L("Error"), wxOK | wxICON_ERROR); + dlg.ShowModal(); +} + +void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&) +{ + bool initial = m_current_computation_initial; + bool auto_color = m_current_computation_auto_color; + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + +#ifdef HAS_WIN10SDK + Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, + _L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."), + _L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT); + dlg.SetButtonLabel(wxID_YES, _L("Import without repair")); + dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true); + // "Repair and import" is the recommended action here, so the accent moves off the default YES + // button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling + // with the same type swaps only the palette and leaves the geometry alone. + if (auto* yes_btn = dynamic_cast(dlg.FindWindow(wxID_YES))) { + yes_btn->SetStyle(ButtonStyle::Regular, ButtonType::Choice); + yes_btn->SetMinSize(wxSize(FromDIP(180), FromDIP(24))); + } + if (auto* no_btn = dynamic_cast(dlg.FindWindow(wxID_NO))) { + no_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); + no_btn->SetMinSize(wxSize(FromDIP(160), FromDIP(24))); + } + dlg.Layout(); + dlg.Fit(); + dlg.CenterOnParent(); + int ret = dlg.ShowModal(); + m_mesh_repair_decision = (ret == wxID_NO) + ? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport + : Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; +#else + Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, + _L("Please note that the mesh has non-manifold geometry or open boundaries."), + _L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT); + dlg.SetButtonLabel(wxID_OK, _L("Continue"), true); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) { + m_cancel_flag = true; + if (initial) { + m_initial_computation_cancelled = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + } else if (has_valid_result()) { + set_state(TextureImportState::Ready); + } else { + set_state(TextureImportState::Idle); + } + return; + } + m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; +#endif + + start_computation(auto_color, initial); +} + +// ---- Mapping ---- + +void TextureImportDialog::update_filament_color_map() +{ + std::map, std::array> color_map; + for (const auto& m : m_current_matches) { + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) { + color_map[m.cluster_color] = { + m_filament_colors_rgba[m.filament_index][0], + m_filament_colors_rgba[m.filament_index][1], + m_filament_colors_rgba[m.filament_index][2] + }; + } + } + m_preview_canvas->set_filament_color_map(color_map); +} + +// Canonical ordering used on the very first display after a computation: +// sort ascending by filament_index, and push unmapped (filament_index < 0) +// entries to the end. This gives the user a stable, predictable mapping +// layout regardless of the cluster discovery order. +void TextureImportDialog::sort_current_matches_by_filament_index() +{ + std::stable_sort(m_current_matches.begin(), m_current_matches.end(), + [](const auto& lhs, const auto& rhs) { + const bool lhs_valid = lhs.filament_index >= 0; + const bool rhs_valid = rhs.filament_index >= 0; + + if (lhs_valid != rhs_valid) + return lhs_valid; + if (!lhs_valid) + return false; + + return lhs.filament_index < rhs.filament_index; + }); +} + +// Preserve the row order the user is currently looking at across a +// re-computation (e.g. when auto-merge is toggled). We key on cluster_index +// because it survives compact_used_virtual_filaments() and filament-index +// renumbering, whereas filament_index does not. +// +// Behaviour: +// * Entries whose cluster_index appeared in `previous_matches` keep their +// previous relative order. +// * Entries whose cluster_index is new (not in `previous_matches`) are +// appended at the end, in their current relative order. +// +// Assumption: each cluster_index appears at most once in both vectors. This +// is currently guaranteed by do_auto_match(), which emits exactly one match +// per cluster. If that invariant ever changes, the std::map::emplace below +// silently keeps only the first occurrence and the order will be wrong. +void TextureImportDialog::restore_current_match_order(const std::vector& previous_matches) +{ + if (previous_matches.empty() || m_current_matches.size() < 2) + return; + + std::map previous_order_by_cluster; + for (size_t i = 0; i < previous_matches.size(); ++i) { + if (previous_matches[i].cluster_index >= 0) + previous_order_by_cluster.emplace(previous_matches[i].cluster_index, i); + } + + std::stable_sort(m_current_matches.begin(), m_current_matches.end(), + [&previous_order_by_cluster](const auto& lhs, const auto& rhs) { + const auto lhs_it = previous_order_by_cluster.find(lhs.cluster_index); + const auto rhs_it = previous_order_by_cluster.find(rhs.cluster_index); + const bool lhs_known = lhs_it != previous_order_by_cluster.end(); + const bool rhs_known = rhs_it != previous_order_by_cluster.end(); + + if (lhs_known != rhs_known) + return lhs_known; + if (!lhs_known) + return false; + + return lhs_it->second < rhs_it->second; + }); +} + +size_t TextureImportDialog::max_filament_count() const +{ + return static_cast(EnforcerBlockerType::ExtruderMax); +} + +bool TextureImportDialog::can_add_virtual_filament() const +{ + return m_filament_colors_rgba.size() < max_filament_count(); +} + +int TextureImportDialog::find_closest_filament_index(const std::array& color) const +{ + int best_idx = -1; + double best_delta = std::numeric_limits::max(); + const size_t filament_count = std::min(m_filament_colors_rgba.size(), max_filament_count()); + for (size_t i = 0; i < filament_count; ++i) { + const double delta = Slic3r::compute_delta_e(color, m_filament_colors_rgba[i]); + if (delta < best_delta) { + best_delta = delta; + best_idx = (int)i; + } + } + return best_idx; +} + +int TextureImportDialog::add_virtual_filament(const std::array& rgba, const std::string& hex, + const std::string& preset_name) +{ + if (m_filament_color_strs.size() != m_filament_colors_rgba.size() || + m_filament_names.size() != m_filament_colors_rgba.size() || + m_filament_entries.size() != m_filament_colors_rgba.size()) { + return -1; + } + if (!can_add_virtual_filament()) { + // Mark that this do_auto_match() run hit the filament cap and had to + // drop at least one cluster. The mapping itself still falls back via + // find_closest_filament_index() below; this flag only drives the + // inline orange warning above the bottom buttons. + // Note: only the false -> true transition happens here; the flag is + // cleared exclusively at the entry of do_auto_match() so it always + // reflects the most recent match, never an accumulated history. + m_filaments_dropped = true; + return -1; + } + + const int new_idx = (int)m_filament_colors_rgba.size(); + m_filament_colors_rgba.push_back(rgba); + m_filament_color_strs.push_back(hex); + m_filament_names.push_back(DEFAULT_VIRTUAL_FILAMENT_NAME); + TextureFilamentEntry entry; + entry.kind = TextureFilamentKind::NewPhysical; + entry.dialog_index = new_idx; + entry.project_config_index = size_t(-1); + entry.color_hex = texture_normalize_color_hex(hex); + entry.name = DEFAULT_VIRTUAL_FILAMENT_NAME; + entry.preset_name = preset_name.empty() ? m_default_virtual_filament_preset_name : preset_name; + m_filament_entries.push_back(entry); + m_new_filament_colors.push_back(rgba); + m_new_filament_preset_names.push_back(preset_name.empty() ? m_default_virtual_filament_preset_name : preset_name); + return new_idx; +} + +int TextureImportDialog::add_virtual_mixed_filament(const std::string& color_hex, + const std::vector& component_dialog_indices, + const std::vector& ratios) +{ + if (m_filament_color_strs.size() != m_filament_colors_rgba.size() || + m_filament_names.size() != m_filament_colors_rgba.size() || + m_filament_entries.size() != m_filament_colors_rgba.size()) { + return -1; + } + if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size()) + return -1; + for (int idx : component_dialog_indices) { + if (idx < 0 || idx >= (int)m_filament_entries.size() || + !texture_entry_is_physical(m_filament_entries[idx].kind)) { + return -1; + } + } + if (!can_add_virtual_filament()) { + m_filaments_dropped = true; + return -1; + } + + const int new_idx = (int)m_filament_colors_rgba.size(); + TextureFilamentEntry entry; + entry.kind = TextureFilamentKind::NewMixed; + entry.dialog_index = new_idx; + entry.project_config_index = size_t(-1); + entry.color_hex = texture_normalize_color_hex(color_hex); + entry.name = DEFAULT_VIRTUAL_FILAMENT_NAME; + entry.mixed_ratios = ratios; + for (int idx : component_dialog_indices) + entry.mixed_components.push_back((unsigned int)(idx + 1)); + + TextureNewMixedFilament mixed; + mixed.dialog_index = entry.dialog_index; + mixed.color_hex = entry.color_hex; + mixed.component_dialog_indices = component_dialog_indices; + mixed.ratios = ratios; + + m_filament_entries.push_back(entry); + m_filament_color_strs.push_back(entry.color_hex); + m_filament_names.push_back(entry.name); + m_filament_colors_rgba.push_back(parse_color_string(entry.color_hex)); + m_new_mixed_filaments.push_back(mixed); + return entry.dialog_index; +} + +void TextureImportDialog::compact_used_virtual_filaments() +{ + if (m_current_matches.empty()) + return; + + const std::vector> old_colors = m_filament_colors_rgba; + const std::vector old_color_strs = m_filament_color_strs; + const std::vector old_names = m_filament_names; + const std::vector old_entries = m_filament_entries; + + auto old_new_mixed_has_valid_components = [&old_entries, &old_colors](const TextureFilamentEntry& entry) { + if (entry.kind != TextureFilamentKind::NewMixed) + return true; + if (entry.mixed_components.size() < 2 || entry.mixed_components.size() != entry.mixed_ratios.size()) + return false; + for (unsigned int comp : entry.mixed_components) { + int comp_idx = comp >= 1 ? (int)comp - 1 : -1; + if (comp_idx < 0 || comp_idx >= (int)old_entries.size() || comp_idx >= (int)old_colors.size() || + !texture_entry_is_physical(old_entries[comp_idx].kind)) { + return false; + } + } + return true; + }; + + std::set used_virtual_indices; + for (const auto& m : m_current_matches) { + if (m.filament_index >= (int)m_existing_filament_count && + m.filament_index < (int)old_colors.size()) { + if (m.filament_index < (int)old_entries.size() && + old_entries[m.filament_index].kind == TextureFilamentKind::NewMixed && + !old_new_mixed_has_valid_components(old_entries[m.filament_index])) { + continue; + } + used_virtual_indices.insert(m.filament_index); + } + } + bool added_dependency = true; + while (added_dependency) { + added_dependency = false; + std::vector current_used(used_virtual_indices.begin(), used_virtual_indices.end()); + for (int used_idx : current_used) { + if (used_idx < 0 || used_idx >= (int)old_entries.size() || + old_entries[used_idx].kind != TextureFilamentKind::NewMixed || + !old_new_mixed_has_valid_components(old_entries[used_idx])) + continue; + for (unsigned int comp : old_entries[used_idx].mixed_components) { + int comp_idx = comp >= 1 ? (int)comp - 1 : -1; + if (comp_idx >= (int)m_existing_filament_count && comp_idx < (int)old_entries.size() && + used_virtual_indices.insert(comp_idx).second) { + added_dependency = true; + } + } + } + } + + std::vector> compact_colors; + std::vector compact_color_strs; + std::vector compact_names; + std::vector compact_entries; + compact_colors.reserve(m_existing_filament_count + used_virtual_indices.size()); + compact_color_strs.reserve(m_existing_filament_count + used_virtual_indices.size()); + compact_names.reserve(m_existing_filament_count + used_virtual_indices.size()); + compact_entries.reserve(m_existing_filament_count + used_virtual_indices.size()); + + const size_t existing_count = std::min(m_existing_filament_count, old_colors.size()); + for (size_t i = 0; i < existing_count; ++i) { + compact_colors.push_back(old_colors[i]); + compact_color_strs.push_back(i < old_color_strs.size() ? old_color_strs[i] : ""); + compact_names.push_back(i < old_names.size() ? old_names[i] : "Filament " + std::to_string(i + 1)); + TextureFilamentEntry entry = i < old_entries.size() ? old_entries[i] : TextureFilamentEntry{}; + entry.dialog_index = (int)i; + entry.color_hex = texture_normalize_color_hex(compact_color_strs.back()); + entry.name = compact_names.back(); + compact_entries.push_back(entry); + } + + std::map old_to_new; + std::vector> compact_new_colors; + std::vector compact_new_preset_names; + compact_new_colors.reserve(used_virtual_indices.size()); + compact_new_preset_names.reserve(used_virtual_indices.size()); + + for (int old_idx : used_virtual_indices) { + old_to_new[old_idx] = (int)compact_colors.size(); + compact_colors.push_back(old_colors[old_idx]); + compact_color_strs.push_back(old_idx < (int)old_color_strs.size() ? old_color_strs[old_idx] : ""); + compact_names.push_back(old_idx < (int)old_names.size() ? old_names[old_idx] : DEFAULT_VIRTUAL_FILAMENT_NAME); + TextureFilamentEntry entry = old_idx < (int)old_entries.size() ? old_entries[old_idx] : TextureFilamentEntry{}; + entry.dialog_index = (int)compact_entries.size(); + entry.color_hex = texture_normalize_color_hex(compact_color_strs.back()); + entry.name = compact_names.back(); + if (entry.kind == TextureFilamentKind::NewPhysical) { + compact_new_colors.push_back(old_colors[old_idx]); + compact_new_preset_names.push_back(entry.preset_name.empty() ? m_default_virtual_filament_preset_name : entry.preset_name); + } + compact_entries.push_back(entry); + } + + m_filament_colors_rgba = std::move(compact_colors); + m_filament_color_strs = std::move(compact_color_strs); + m_filament_names = std::move(compact_names); + m_filament_entries = std::move(compact_entries); + m_new_filament_colors = std::move(compact_new_colors); + m_new_filament_preset_names = std::move(compact_new_preset_names); + m_new_mixed_filaments.clear(); + std::set invalid_compacted_mixed_indices; + for (auto& entry : m_filament_entries) { + if (entry.kind != TextureFilamentKind::NewMixed) + continue; + TextureNewMixedFilament mixed; + mixed.dialog_index = entry.dialog_index; + mixed.color_hex = entry.color_hex; + mixed.ratios = entry.mixed_ratios; + mixed.component_dialog_indices.reserve(entry.mixed_components.size()); + bool valid_components = entry.mixed_components.size() >= 2 && + entry.mixed_components.size() == entry.mixed_ratios.size(); + for (unsigned int comp : entry.mixed_components) { + int old_comp_idx = comp >= 1 ? (int)comp - 1 : -1; + if (old_comp_idx < 0) { + valid_components = false; + break; + } + auto remap_it = old_to_new.find(old_comp_idx); + int new_comp_idx = remap_it != old_to_new.end() ? remap_it->second : old_comp_idx; + if (new_comp_idx < 0 || new_comp_idx >= (int)m_filament_entries.size() || + !texture_entry_is_physical(m_filament_entries[new_comp_idx].kind)) { + valid_components = false; + break; + } + mixed.component_dialog_indices.push_back(new_comp_idx); + } + if (!valid_components) { + invalid_compacted_mixed_indices.insert(entry.dialog_index); + continue; + } + entry.mixed_components.clear(); + for (int comp_idx : mixed.component_dialog_indices) + entry.mixed_components.push_back((unsigned int)(comp_idx + 1)); + m_new_mixed_filaments.push_back(mixed); + } + + auto find_closest_physical_filament_index = [this](const std::array& color) { + int best_idx = -1; + double best_delta = std::numeric_limits::max(); + const size_t filament_count = std::min(m_filament_colors_rgba.size(), max_filament_count()); + for (size_t i = 0; i < filament_count && i < m_filament_entries.size(); ++i) { + if (!texture_entry_is_physical(m_filament_entries[i].kind)) + continue; + const double delta = Slic3r::compute_delta_e(color, m_filament_colors_rgba[i]); + if (delta < best_delta) { + best_delta = delta; + best_idx = (int)i; + } + } + return best_idx; + }; + + for (auto& m : m_current_matches) { + auto it = old_to_new.find(m.filament_index); + if (it != old_to_new.end()) { + m.filament_index = it->second; + } else if (m.filament_index >= (int)m_existing_filament_count) { + m.filament_index = find_closest_filament_index(m.cluster_color); + } + if (invalid_compacted_mixed_indices.count(m.filament_index) > 0) { + int fallback_idx = find_closest_physical_filament_index(m.cluster_color); + m.filament_index = fallback_idx >= 0 ? fallback_idx : find_closest_filament_index(m.cluster_color); + } + + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) { + m.filament_color = m_filament_colors_rgba[m.filament_index]; + m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color); + if (m.filament_index >= (int)m_existing_filament_count) + m.delta_e = 0.0; + } + } +} + +std::vector TextureImportDialog::compute_display_numbers() const +{ + // Assigns each entry a 1-based display number in the order the sidebar will + // show after apply: ExistingPhysical, NewPhysical, ExistingMixed, NewMixed. + // This keeps the dialog's visible IDs in sync with the post-apply sidebar, + // instead of the raw dialog_index (which interleaves physicals and mixeds + // by processing order and causes e.g. CMYW to show 4,5,6,8 instead of 3,4,5,6). + // MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896): + // - ExistingPhysical keeps its project_config_index + // - NewPhysical is inserted at existing_physical_count + new_order + // - ExistingMixed shifts to project_config_index + new_physical_count + // - NewMixed is appended after all existing mixeds + std::vector result(m_filament_entries.size(), 0); + int next = 1; + + auto assign_group = [&](TextureFilamentKind kind, bool by_project_config_index) { + if (by_project_config_index) { + std::vector group; + for (const auto& e : m_filament_entries) + if (e.kind == kind) + group.push_back(&e); + std::sort(group.begin(), group.end(), + [](const TextureFilamentEntry* a, const TextureFilamentEntry* b) { + return a->project_config_index < b->project_config_index; + }); + for (const auto* e : group) { + if (e->dialog_index >= 0 && e->dialog_index < (int)result.size()) + result[e->dialog_index] = next; + ++next; + } + } else { + for (const auto& e : m_filament_entries) { + if (e.kind != kind) + continue; + if (e.dialog_index >= 0 && e.dialog_index < (int)result.size()) + result[e.dialog_index] = next; + ++next; + } + } + }; + + assign_group(TextureFilamentKind::ExistingPhysical, true); + assign_group(TextureFilamentKind::NewPhysical, false); + assign_group(TextureFilamentKind::ExistingMixed, true); + assign_group(TextureFilamentKind::NewMixed, false); + return result; +} + +void TextureImportDialog::dismiss_filament_popup() +{ + if (!m_filament_popup) { + m_filament_popup_row = -1; + return; + } + + FilamentSelectPopup* popup = m_filament_popup; + m_filament_popup = nullptr; + m_filament_popup_row = -1; + if (popup->IsShown()) + popup->Dismiss(); + else + popup->Destroy(); +} + +void TextureImportDialog::show_auto_mix_popup() +{ + if (!m_btn_auto_mix || !m_btn_auto_mix->IsEnabled()) + return; + + if (m_auto_mix_popup && m_auto_mix_popup->IsShown()) + return; + dismiss_auto_mix_popup(); + + auto on_select = [this](TextureAutoMixMode mode) { + set_auto_mix_mode(mode); + }; + auto on_close = [this]() { + m_auto_mix_popup = nullptr; + }; + + auto* popup = new AutoMixSelectPopup(this, m_auto_mix_mode, m_btn_auto_mix->GetSize().x, + m_auto_mix_font_point_size, + on_select, on_close); + wxPoint pos = m_btn_auto_mix->ClientToScreen(wxPoint(0, m_btn_auto_mix->GetSize().y)); + wxRect display_rect; + int display_idx = wxDisplay::GetFromPoint(pos); + if (display_idx != wxNOT_FOUND) + display_rect = wxDisplay(display_idx).GetClientArea(); + else + display_rect = wxDisplay().GetClientArea(); + pos.x = std::clamp(pos.x, display_rect.GetLeft(), + std::max(display_rect.GetLeft(), display_rect.GetRight() - popup->GetSize().x)); + popup->Position(pos, wxSize(0, 0)); + popup->Bind(wxEVT_DESTROY, [this, popup](wxWindowDestroyEvent& e) { + e.Skip(); + if (m_auto_mix_popup == popup) + m_auto_mix_popup = nullptr; + }); + m_auto_mix_popup = popup; + popup->Popup(); +} + +void TextureImportDialog::dismiss_auto_mix_popup() +{ + if (!m_auto_mix_popup) + return; + + AutoMixSelectPopup* popup = m_auto_mix_popup; + m_auto_mix_popup = nullptr; + if (popup->IsShown()) + popup->Dismiss(); + else + popup->Destroy(); +} + +void TextureImportDialog::set_auto_mix_mode(TextureAutoMixMode mode) +{ + m_auto_mix_mode = mode; + if (m_btn_auto_mix) { + m_btn_auto_mix->SetLabel(auto_mix_mode_label(mode)); + m_btn_auto_mix->Refresh(); + } + apply_auto_standard_mix(mode); +} + +void TextureImportDialog::apply_auto_standard_mix(TextureAutoMixMode mode) +{ + if (m_mapping_rows.empty()) + return; + m_filaments_dropped = false; + + auto find_or_add_base_physical = [this](const std::string& color_hex) -> int { + const std::string normalized = texture_normalize_color_hex(color_hex); + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_physical(entry.kind)) + continue; + if (texture_normalize_color_hex(entry.color_hex) != normalized) + continue; + if (texture_entry_is_pla_basic(entry)) + return entry.dialog_index; + } + + std::array rgba = parse_color_string(normalized); + int idx = add_virtual_filament(rgba, normalized, m_default_virtual_filament_preset_name); + if (idx >= 0 && idx < (int)m_filament_entries.size()) { + m_filament_entries[idx].type = DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE; + m_filament_entries[idx].name = DEFAULT_VIRTUAL_FILAMENT_NAME; + } + return idx; + }; + + auto find_existing_mixed = [this](const std::vector& component_indices, const std::vector& ratios) -> int { + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_mixed(entry.kind) || entry.mixed_components.size() != component_indices.size() || + entry.mixed_ratios.size() != ratios.size()) + continue; + bool same = true; + for (size_t i = 0; i < component_indices.size(); ++i) { + if (entry.mixed_components[i] != (unsigned int)(component_indices[i] + 1) || + entry.mixed_ratios[i] != ratios[i]) { + same = false; + break; + } + } + if (same) + return entry.dialog_index; + } + return -1; + }; + + bool changed = false; + const auto recipe_mode = texture_recipe_mode(mode); + for (size_t row_index = 0; row_index < m_mapping_rows.size(); ++row_index) { + Slic3r::ColorDecomposeRgb target_rgb; + if (!Slic3r::color_decompose_hex_to_rgb(m_mapping_rows[row_index].source_hex, target_rgb)) + continue; + + auto recipe = Slic3r::lookup_standard_recipe(target_rgb, recipe_mode, DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE); + if (!recipe.valid || recipe.components.size() < 2) + continue; + + std::vector component_dialog_indices; + std::vector ratios; + for (const auto& comp : recipe.components) { + int component_idx = find_or_add_base_physical(comp.color_hex); + if (component_idx < 0) { + component_dialog_indices.clear(); + break; + } + component_dialog_indices.push_back(component_idx); + ratios.push_back(comp.ratio); + } + if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size()) + continue; + + int mixed_idx = find_existing_mixed(component_dialog_indices, ratios); + if (mixed_idx < 0) + mixed_idx = add_virtual_mixed_filament(recipe.matched_color_hex, component_dialog_indices, ratios); + if (mixed_idx < 0) + continue; + + m_mapping_rows[row_index].target_filament_idx = mixed_idx; + if (row_index < m_current_matches.size()) { + m_current_matches[row_index].filament_index = mixed_idx; + m_current_matches[row_index].filament_color = m_filament_colors_rgba[mixed_idx]; + m_current_matches[row_index].delta_e = Slic3r::compute_delta_e( + m_current_matches[row_index].cluster_color, m_current_matches[row_index].filament_color); + if (mixed_idx >= (int)m_existing_filament_count) + m_current_matches[row_index].delta_e = 0.0; + } + changed = true; + } + + if (!changed) + return; + + m_auto_mix_applied = true; + compact_used_virtual_filaments(); + update_filament_color_map(); + rebuild_mapping_rows(); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); +} + +void TextureImportDialog::reset_auto_mix() +{ + if (m_state != TextureImportState::Ready || !m_auto_mix_applied) + return; + + dismiss_auto_mix_popup(); + + // Clear mixed filament references so the compact inside do_auto_match() + // removes them (and their exclusively-owned base physicals) from the + // filament arrays, giving the baseline matching a clean starting state. + for (auto& m : m_current_matches) { + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_entries.size() && + texture_entry_is_mixed(m_filament_entries[m.filament_index].kind)) { + m.filament_index = -1; + } + } + + // Re-run the baseline auto-match (same flow as the auto-merge toggle) so the + // mapping reverts to the pre-mix state: every colour matches an existing + // physical filament or a virtual physical filament, with no mixed filaments. + const auto previous_matches = m_current_matches; + do_auto_match(); + restore_current_match_order(previous_matches); + compact_used_virtual_filaments(); + update_filament_color_map(); + rebuild_mapping_rows(); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); +} + +void TextureImportDialog::update_auto_mix_reset_visibility() +{ + if (!m_btn_mix_reset) + return; + if (m_btn_mix_reset->Show(m_auto_mix_applied)) { + if (wxWindow* parent = m_btn_mix_reset->GetParent()) + parent->Layout(); + } +} + +bool TextureImportDialog::add_decomposed_mixed_filament(size_t row_index) +{ + if (row_index >= m_mapping_rows.size()) + return false; + + std::vector physical_colors; + std::vector physical_names; + std::vector physical_types; + std::vector physical_dialog_indices; + std::vector physical_config_indices; + auto& preset_bundle = *wxGetApp().preset_bundle; + for (const auto& entry : m_filament_entries) { + if (entry.kind != TextureFilamentKind::ExistingPhysical) + continue; + physical_colors.push_back(entry.color_hex); + physical_names.push_back(entry.name); + const size_t cfg_idx = entry.project_config_index; + Preset* preset = nullptr; + if (cfg_idx < preset_bundle.filament_presets.size()) + preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]); + physical_types.push_back(filament_type_for_color_decompose(preset)); + physical_dialog_indices.push_back(entry.dialog_index); + physical_config_indices.push_back(cfg_idx); + } + if (physical_colors.empty()) + return false; + + wxColour target(m_mapping_rows[row_index].source_hex); + ColorDecomposeDialog dlg(this, -1, target, physical_colors, physical_names, physical_types, + m_filament_entries.size(), max_filament_count(), + std::move(physical_config_indices)); + // Count "new physical filaments" with the exact reuse rule of the write-back + // loop below: a base color is only new if no existing OR virtual official + // Bambu Basic filament already carries that color. This keeps the dialog's + // filament-limit pre-check consistent with what add_decomposed_mixed_filament + // will actually create, so already-present virtual base colors are not + // double counted (which previously could wrongly disable OK). + dlg.set_missing_physical_calculator([this](const ColorDecomposeResult& result) -> size_t { + size_t missing = 0; + for (const DecomposeComponent& comp : result.components) { + if (comp.filament_index > 0) + continue; // reuses a physical slot passed to the dialog, no new filament + const std::string comp_hex = texture_normalize_color_hex( + comp.colour.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + bool found = false; + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_physical(entry.kind)) + continue; + if (texture_normalize_color_hex(entry.color_hex) != comp_hex) + continue; + if (texture_entry_official_basic(entry)) { + found = true; + break; + } + } + if (!found) + ++missing; + } + return missing; + }); + if (dlg.ShowModal() != wxID_OK) + return false; + + ColorDecomposeResult result = dlg.get_result(); + std::vector component_dialog_indices; + std::vector ratios; + for (const DecomposeComponent& comp : result.components) { + ratios.push_back(comp.ratio); + if (comp.filament_index > 0) { + const size_t physical_idx = (size_t)(comp.filament_index - 1); + if (physical_idx >= physical_dialog_indices.size()) + return false; + component_dialog_indices.push_back(physical_dialog_indices[physical_idx]); + continue; + } + + const std::string comp_hex = texture_normalize_color_hex(comp.colour.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + int existing_idx = -1; + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_physical(entry.kind)) + continue; + if (texture_normalize_color_hex(entry.color_hex) != comp_hex) + continue; + if (texture_entry_official_basic(entry)) { + existing_idx = entry.dialog_index; + break; + } + } + if (existing_idx < 0) { + std::array rgba = parse_color_string(comp_hex); + existing_idx = add_virtual_filament(rgba, comp_hex); + if (existing_idx < 0) + return false; + } + component_dialog_indices.push_back(existing_idx); + } + + if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size()) + return false; + + const std::string mixed_hex = texture_normalize_color_hex( + result.matched_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + int mixed_idx = add_virtual_mixed_filament(mixed_hex, component_dialog_indices, ratios); + if (mixed_idx < 0) + return false; + + m_mapping_rows[row_index].target_filament_idx = mixed_idx; + if (row_index < m_current_matches.size()) + m_current_matches[row_index].filament_index = mixed_idx; + rebuild_mapping_rows(); + update_filament_color_map(); + return true; +} + +void TextureImportDialog::dismiss_filament_popup_on_wheel(wxMouseEvent& evt) +{ + dismiss_filament_popup(); + dismiss_auto_mix_popup(); + evt.Skip(); +} + +void TextureImportDialog::show_filament_popup(size_t row_index) +{ + if (row_index >= m_mapping_rows.size()) return; + + if (m_skip_next_filament_popup_row == (int)row_index) { + m_skip_next_filament_popup_row = -1; + return; + } + + if (m_filament_popup && m_filament_popup->IsShown()) { + if (m_filament_popup_row == (int)row_index) { + dismiss_filament_popup(); + return; + } + dismiss_filament_popup(); + } + + const auto display_numbers = compute_display_numbers(); + auto display_number = [display_numbers](int idx) -> int { + return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0) + ? display_numbers[idx] : idx + 1; + }; + + auto on_select = [this, row_index, display_number](int idx) { + if (row_index >= m_mapping_rows.size()) return; + m_mapping_rows[row_index].target_filament_idx = idx; + if (row_index < m_current_matches.size()) + m_current_matches[row_index].filament_index = idx; + if (m_mapping_rows[row_index].target_panel) { + wxString label = (idx >= 0 && idx < (int)m_filament_names.size()) + ? filament_name_to_wx_string(m_filament_names[idx]) + : wxString::Format("Filament %d", display_number(idx)); + m_mapping_rows[row_index].target_panel->SetToolTip(label); + m_mapping_rows[row_index].target_panel->Refresh(); + } + update_filament_color_map(); + }; + + auto on_add_filament = [this, row_index](wxColour clr) { + std::array rgba = {clr.Red() / 255.f, clr.Green() / 255.f, + clr.Blue() / 255.f, 1.0f}; + std::string hex = wxString::Format("#%02X%02X%02X", + clr.Red(), clr.Green(), clr.Blue()).ToStdString(); + int new_idx = add_virtual_filament(rgba, hex); + if (new_idx < 0) + return; + + if (row_index < m_mapping_rows.size()) { + m_mapping_rows[row_index].target_filament_idx = new_idx; + if (row_index < m_current_matches.size()) + m_current_matches[row_index].filament_index = new_idx; + } + rebuild_mapping_rows(); + update_filament_color_map(); + }; + + auto on_decompose_color = [this, row_index]() { + CallAfter([this, row_index]() { + add_decomposed_mixed_filament(row_index); + }); + }; + + wxPanel* tp = m_mapping_rows[row_index].target_panel; + if (!tp) return; + + auto on_close = [this, row_index](bool closed_by_action) { + if (m_filament_popup_row == (int)row_index) { + m_filament_popup = nullptr; + m_filament_popup_row = -1; + } + if (!closed_by_action) { + m_skip_next_filament_popup_row = (int)row_index; + CallAfter([this, row_index]() { + if (m_skip_next_filament_popup_row == (int)row_index) + m_skip_next_filament_popup_row = -1; + }); + } + }; + + auto* popup = new FilamentSelectPopup( + this, m_filament_entries, m_filament_colors_rgba, m_filament_names, + m_existing_filament_count, tp->GetSize().x, tp, on_select, on_add_filament, + on_decompose_color, + [this]() { return can_add_virtual_filament(); }, + on_close, + display_numbers); + + wxPoint pos = tp->ClientToScreen(wxPoint(0, tp->GetSize().y)); + wxRect display_rect; + int display_idx = wxDisplay::GetFromPoint(pos); + if (display_idx != wxNOT_FOUND) + display_rect = wxDisplay(display_idx).GetClientArea(); + else + display_rect = wxDisplay().GetClientArea(); + pos.x = std::clamp(pos.x, display_rect.GetLeft(), + std::max(display_rect.GetLeft(), display_rect.GetRight() - popup->GetSize().x)); + popup->Position(pos, wxSize(0, 0)); + popup->Bind(wxEVT_DESTROY, [this, popup](wxWindowDestroyEvent& e) { + e.Skip(); + if (m_filament_popup == popup) { + m_filament_popup = nullptr; + m_filament_popup_row = -1; + } + }); + m_filament_popup = popup; + m_filament_popup_row = (int)row_index; + popup->Popup(); +} + +void TextureImportDialog::do_auto_match() +{ + if (m_painted.cluster_colors.empty()) return; + + // do_auto_match() always rebuilds the baseline mapping without any mixed + // filaments, so it is the common entry for every "revert one-click mix" + // path. Clear the applied flag here; callers refresh the reset button. + m_auto_mix_applied = false; + + // Reset the "filaments were dropped" flag at the start of every run, so it + // strictly reflects what happens during *this* match (no historical + // accumulation). add_virtual_filament() will flip it back to true if and + // only if it hits the global filament cap below. + m_filaments_dropped = false; + + // Drop any virtual filaments left over from previous match runs that the + // current m_current_matches no longer references. Without this, the + // residual virtual filaments inflate m_filament_colors_rgba.size() at the + // entry of this match, which can make add_virtual_filament() fail (and + // wrongly flip m_filaments_dropped to true) even when the *real* count + // of needed virtual filaments for this run is well below the global cap. + // This is purely a state cleanup; it does not change any mapping rule. + compact_used_virtual_filaments(); + + const auto previous_matches = m_current_matches; + + std::map, int> previous_virtual_by_cluster; + for (const auto& match : previous_matches) { + if (match.filament_index >= (int)m_existing_filament_count && + match.filament_index < (int)m_filament_entries.size() && + texture_entry_is_physical(m_filament_entries[match.filament_index].kind)) { + previous_virtual_by_cluster[match.cluster_color] = match.filament_index; + } + } + + auto find_virtual_filament_by_color = [this](const std::array& color) -> int { + std::string hex = rgb_to_hex(color).ToStdString(); + for (size_t i = m_existing_filament_count; i < m_filament_color_strs.size(); ++i) { + if (m_filament_color_strs[i] == hex && + i < m_filament_entries.size() && texture_entry_is_physical(m_filament_entries[i].kind)) + return (int)i; + } + return -1; + }; + + auto get_or_add_virtual_filament = [this, &previous_virtual_by_cluster, &find_virtual_filament_by_color]( + const std::array& color) -> int { + auto previous_it = previous_virtual_by_cluster.find(color); + if (previous_it != previous_virtual_by_cluster.end() && + previous_it->second >= (int)m_existing_filament_count && + previous_it->second < (int)m_filament_colors_rgba.size()) { + return previous_it->second; + } + + int existing_idx = find_virtual_filament_by_color(color); + if (existing_idx >= 0) + return existing_idx; + + std::array rgba = { + color[0] / 255.f, + color[1] / 255.f, + color[2] / 255.f, + 1.f + }; + return add_virtual_filament(rgba, rgb_to_hex(color).ToStdString()); + }; + + if (m_auto_merge_cb && m_auto_merge_cb->GetValue()) { + // Match clusters to closest existing filaments + std::vector names; + for (size_t i = 0; i < m_existing_filament_count; ++i) + names.push_back(m_filament_names.size() > i ? m_filament_names[i] : "Filament " + std::to_string(i + 1)); + + std::vector> existing_filament_colors( + m_filament_colors_rgba.begin(), + m_filament_colors_rgba.begin() + std::min(m_existing_filament_count, m_filament_colors_rgba.size())); + + m_current_matches = Slic3r::match_clusters_to_filaments( + m_painted.cluster_colors, existing_filament_colors, names); + + // For clusters with poor match (CIEDE2000 ΔE > 5), create virtual filaments. + constexpr double NEW_FILAMENT_THRESHOLD = 5.0; + std::map, int> virtual_color_index; + + for (auto& m : m_current_matches) { + if (m.delta_e <= NEW_FILAMENT_THRESHOLD) + continue; + + auto it = virtual_color_index.find(m.cluster_color); + if (it != virtual_color_index.end()) { + m.filament_index = it->second; + } else { + int new_idx = get_or_add_virtual_filament(m.cluster_color); + if (new_idx >= 0) + virtual_color_index[m.cluster_color] = new_idx; + m.filament_index = new_idx >= 0 ? new_idx : find_closest_filament_index(m.cluster_color); + } + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) { + m.filament_color = m_filament_colors_rgba[m.filament_index]; + m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color); + if (m.filament_index >= (int)m_existing_filament_count) + m.delta_e = 0.0; + } + } + } else { + // Keep all virtual filaments in this dialog; unused ones are pruned only on OK. + m_current_matches.clear(); + std::map, int> virtual_map; + + for (size_t i = 0; i < m_painted.cluster_colors.size(); ++i) { + const auto& cc = m_painted.cluster_colors[i]; + Slic3r::FilamentMatch fm; + fm.cluster_index = (int)i; + fm.cluster_color = cc; + + auto it = virtual_map.find(cc); + if (it != virtual_map.end()) { + fm.filament_index = it->second; + } else { + int idx = get_or_add_virtual_filament(cc); + if (idx >= 0) + virtual_map[cc] = idx; + fm.filament_index = idx >= 0 ? idx : find_closest_filament_index(cc); + } + if (fm.filament_index >= 0 && fm.filament_index < (int)m_filament_colors_rgba.size()) { + fm.filament_color = m_filament_colors_rgba[fm.filament_index]; + fm.delta_e = Slic3r::compute_delta_e(fm.cluster_color, fm.filament_color); + if (fm.filament_index >= (int)m_existing_filament_count) + fm.delta_e = 0.0; + } + m_current_matches.push_back(fm); + } + } + + update_filament_color_map(); +} + +void TextureImportDialog::rebuild_mapping_rows() +{ + m_mapping_scroll->Freeze(); + m_mapping_sizer->Clear(true); + m_mapping_rows.clear(); + + if (m_current_matches.empty()) { + m_mapping_scroll->FitInside(); + m_mapping_scroll->Thaw(); + return; + } + + auto get_target_wxcolor = [this](int idx) -> wxColour { + if (idx >= 0 && idx < (int)m_filament_colors_rgba.size()) { + const auto& c = m_filament_colors_rgba[idx]; + return wxColour((unsigned char)(c[0] * 255.f), + (unsigned char)(c[1] * 255.f), + (unsigned char)(c[2] * 255.f)); + } + return wxColour(128, 128, 128); + }; + + const auto display_numbers = compute_display_numbers(); + auto display_number = [display_numbers](int idx) -> int { + return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0) + ? display_numbers[idx] : idx + 1; + }; + + auto get_filament_label = [this, display_number](int idx) -> wxString { + if (idx >= 0 && idx < (int)m_filament_names.size()) + return filament_name_to_wx_string(m_filament_names[idx]); + return wxString::Format("Filament %d", display_number(idx)); + }; + + const wxColour dash_clr = StateColor::darkModeColorFor(wxColour("#ACACAC")); + const wxColour hex_fg = texture_import_text_colour(); + const wxColour card_bg = StateColor::darkModeColorFor(wxColour("#E8E8E8")); + const wxColour card_bd = StateColor::darkModeColorFor(wxColour("#DBDBDB")); + const wxColour name_fg = texture_import_text_colour(); + const wxColour chev_clr = StateColor::darkModeColorFor(wxColour("#6B6B6A")); + + m_mapping_rows.resize(m_current_matches.size()); + for (size_t ci = 0; ci < m_current_matches.size(); ++ci) { + auto& row = m_mapping_rows[ci]; + row.cluster_id = m_current_matches[ci].cluster_index; + row.source_color = m_current_matches[ci].cluster_color; + row.source_hex = rgb_to_hex(row.source_color).ToStdString(); + row.target_filament_idx = m_current_matches[ci].filament_index; + + wxColour src_wx_color( + (unsigned char)row.source_color[0], + (unsigned char)row.source_color[1], + (unsigned char)row.source_color[2]); + + // --- Row container --- + wxPanel* row_panel = new wxPanel(m_mapping_scroll, wxID_ANY); + row_panel->SetBackgroundColour(m_mapping_scroll->GetBackgroundColour()); + row_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + wxBoxSizer* row_sizer = new wxBoxSizer(wxHORIZONTAL); + + // --- Source card (dashed border, circle + hex) --- + const int src_w = FromDIP(138); + const int target_min_w = FromDIP(239); + const int row_h = FromDIP(44); + row.source_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(src_w, row_h), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE); + row.source_panel->SetMinSize(wxSize(src_w, row_h)); + row.source_panel->SetMaxSize(wxSize(src_w, row_h)); + row.source_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + + row.source_panel->Bind(wxEVT_PAINT, [this, ci, src_wx_color, dash_clr, hex_fg](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + wxPen dash_pen(dash_clr, 1, wxPENSTYLE_SHORT_DASH); + dc.SetPen(dash_pen); + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + int r = p->FromDIP(8); + dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, r); + + // Color circle 24px + int cd = p->FromDIP(24); + int cx = p->FromDIP(10); + int cy = (sz.y - cd) / 2; + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(src_wx_color)); + dc.DrawEllipse(cx, cy, cd, cd); + draw_filament_swatch_ellipse_border(dc, src_wx_color, cx, cy, cd, cd); + + if (ci < m_mapping_rows.size()) { + wxFont hex_font = p->GetFont(); + hex_font.SetPointSize(9); + dc.SetFont(hex_font); + dc.SetTextForeground(hex_fg); + wxString hex_str = wxString::Format("# %s", m_mapping_rows[ci].source_hex.substr(1)); + wxSize tsz = dc.GetTextExtent(hex_str); + dc.DrawText(hex_str, cx + cd + p->FromDIP(6), (sz.y - tsz.y) / 2); + } + }); + row.source_panel->Bind(wxEVT_SIZE, [](wxSizeEvent& e) { + e.Skip(); + static_cast(e.GetEventObject())->Refresh(); + }); + row.source_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + + row_sizer->Add(row.source_panel, 0, wxEXPAND); + + // --- Arrow panel (dashed arrow) --- + const int arrow_w = FromDIP(24); + wxPanel* arrow_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(arrow_w, row_h)); + arrow_panel->SetMinSize(wxSize(arrow_w, row_h)); + arrow_panel->SetMaxSize(wxSize(arrow_w, row_h)); + arrow_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + arrow_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + arrow_panel->Bind(wxEVT_PAINT, [dash_clr](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + int mid_y = sz.y / 2; + int margin = p->FromDIP(2); + int arrow_tip = sz.x - margin; + int arrow_start = margin; + + wxPen dash_pen(dash_clr, p->FromDIP(1), wxPENSTYLE_SHORT_DASH); + dc.SetPen(dash_pen); + dc.DrawLine(arrow_start, mid_y, arrow_tip - p->FromDIP(4), mid_y); + + int ah = p->FromDIP(4); + wxPoint tri[3] = { + {arrow_tip, mid_y}, + {arrow_tip - ah, mid_y - ah / 2}, + {arrow_tip - ah, mid_y + ah / 2} + }; + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(dash_clr)); + dc.DrawPolygon(3, tri); + }); + + row_sizer->Add(arrow_panel, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4)); + + // --- Target card (numbered square + material name + chevron) --- + row.target_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE); + row.target_panel->SetMinSize(wxSize(target_min_w, row_h)); + row.target_panel->SetToolTip(get_filament_label(row.target_filament_idx)); + row.target_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + row.target_panel->SetCursor(wxCursor(wxCURSOR_HAND)); + + row.target_panel->Bind(wxEVT_PAINT, [this, ci, get_target_wxcolor, get_filament_label, + display_number, card_bg, card_bd, name_fg, chev_clr](wxPaintEvent& e) { + auto* p = static_cast(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + if (ci >= m_mapping_rows.size()) return; + int fil_idx = m_mapping_rows[ci].target_filament_idx; + + int r = p->FromDIP(8); + dc.SetBrush(wxBrush(card_bg)); + dc.SetPen(wxPen(card_bd, 1)); + dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, r); + + if (fil_idx >= 0 && fil_idx < (int)m_filament_entries.size() && + texture_entry_is_mixed(m_filament_entries[fil_idx].kind)) { + const TextureFilamentEntry& entry = m_filament_entries[fil_idx]; + wxFont mixed_font = p->GetFont(); + mixed_font.SetPointSize(10); + dc.SetFont(mixed_font); + + int x = p->FromDIP(10); + const int sw = p->FromDIP(28); + const int sw_r = p->FromDIP(6); + const int sw_y = (sz.y - sw) / 2; + for (size_t mi = 0; mi < entry.mixed_components.size() && mi < entry.mixed_ratios.size(); ++mi) { + if (mi > 0) { + dc.SetTextForeground(name_fg); + wxString plus = "+"; + wxSize psz = dc.GetTextExtent(plus); + dc.DrawText(plus, x, (sz.y - psz.y) / 2); + x += psz.x + p->FromDIP(4); + } + + const unsigned int comp_id = entry.mixed_components[mi]; + const int comp_idx = comp_id >= 1 ? (int)comp_id - 1 : -1; + wxColour comp_clr("#D9D9D9"); + if (comp_idx >= 0 && comp_idx < (int)m_filament_colors_rgba.size()) { + const auto& c = m_filament_colors_rgba[comp_idx]; + comp_clr = wxColour((unsigned char)(c[0] * 255.f), + (unsigned char)(c[1] * 255.f), + (unsigned char)(c[2] * 255.f)); + } + + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(comp_clr)); + dc.DrawRoundedRectangle(x, sw_y, sw, sw, sw_r); + draw_filament_swatch_border(dc, comp_clr, x, sw_y, sw, sw, sw_r); + + wxString num_str = wxString::Format("%d", display_number(comp_idx)); + wxSize nsz = dc.GetTextExtent(num_str); + dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + dc.DrawText(num_str, x + (sw - nsz.x) / 2, sw_y + (sw - nsz.y) / 2); + x += sw + p->FromDIP(5); + + dc.SetTextForeground(name_fg); + wxString pct = wxString::Format("%d%%", entry.mixed_ratios[mi]); + wxSize pct_sz = dc.GetTextExtent(pct); + dc.DrawText(pct, x, (sz.y - pct_sz.y) / 2); + x += pct_sz.x + p->FromDIP(5); + if (x > sz.x - p->FromDIP(34)) + break; + } + + int chev_cx = sz.x - p->FromDIP(14); + int chev_cy = sz.y / 2; + int hw = p->FromDIP(3); + int hh = p->FromDIP(2); + dc.SetPen(wxPen(chev_clr, p->FromDIP(1) > 0 ? p->FromDIP(1) : 1)); + dc.DrawLine(chev_cx - hw, chev_cy - hh, chev_cx, chev_cy + hh); + dc.DrawLine(chev_cx, chev_cy + hh, chev_cx + hw, chev_cy - hh); + return; + } + + // Numbered color square 32x32, rounded 6px + int sq = p->FromDIP(32); + int sq_x = p->FromDIP(6); + int sq_y = (sz.y - sq) / 2; + int sq_r = p->FromDIP(6); + wxColour fil_clr = get_target_wxcolor(fil_idx); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(fil_clr)); + dc.DrawRoundedRectangle(sq_x, sq_y, sq, sq, sq_r); + draw_filament_swatch_border(dc, fil_clr, sq_x, sq_y, sq, sq, sq_r); + + { + wxFont num_font = p->GetFont(); + num_font.SetPointSize(10); + dc.SetFont(num_font); + dc.SetTextForeground(fil_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + wxString num_str = wxString::Format("%d", display_number(fil_idx)); + wxSize nsz = dc.GetTextExtent(num_str); + dc.DrawText(num_str, sq_x + (sq - nsz.x) / 2, sq_y + (sq - nsz.y) / 2); + } + + // Material name + { + wxFont name_font = p->GetFont(); + name_font.SetPointSize(9); + dc.SetFont(name_font); + dc.SetTextForeground(name_fg); + wxString name_str = get_filament_label(fil_idx); + int text_x = sq_x + sq + p->FromDIP(8); + int max_text_w = sz.x - text_x - p->FromDIP(24); + if (max_text_w > 0) { + name_str = ellipsize_text(dc, name_str, max_text_w); + wxSize tsz = dc.GetTextExtent(name_str); + dc.DrawText(name_str, text_x, (sz.y - tsz.y) / 2); + } + } + + // Dropdown chevron at right edge + { + int chev_cx = sz.x - p->FromDIP(14); + int chev_cy = sz.y / 2; + int hw = p->FromDIP(3); + int hh = p->FromDIP(2); + dc.SetPen(wxPen(chev_clr, p->FromDIP(1) > 0 ? p->FromDIP(1) : 1)); + dc.DrawLine(chev_cx - hw, chev_cy - hh, chev_cx, chev_cy + hh); + dc.DrawLine(chev_cx, chev_cy + hh, chev_cx + hw, chev_cy - hh); + } + }); + + row.target_panel->Bind(wxEVT_LEFT_DOWN, [this, ci](wxMouseEvent&) { + show_filament_popup(ci); + }); + row.target_panel->Bind(wxEVT_SIZE, [](wxSizeEvent& e) { + e.Skip(); + static_cast(e.GetEventObject())->Refresh(); + }); + row.target_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + + row_sizer->Add(row.target_panel, 1, wxEXPAND); + + row_panel->SetSizer(row_sizer); + m_mapping_sizer->Add(row_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(12)); + } + + m_mapping_scroll->FitInside(); + m_mapping_scroll->Layout(); + m_mapping_scroll->Thaw(); +} + +std::vector TextureImportDialog::build_matches_from_rows() const +{ + std::vector matches(m_mapping_rows.size()); + for (size_t i = 0; i < m_mapping_rows.size(); ++i) { + auto& m = matches[i]; + m.cluster_index = m_mapping_rows[i].cluster_id; + m.cluster_color = m_mapping_rows[i].source_color; + + int sel = m_mapping_rows[i].target_filament_idx; + if (sel >= 0 && sel < (int)m_filament_colors_rgba.size()) { + m.filament_index = sel; + m.filament_color = m_filament_colors_rgba[sel]; + m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color); + } + } + return matches; +} + +// ---- Event handlers ---- + +void TextureImportDialog::update_color_count_preset_buttons() +{ + if (m_btn_color_4) m_btn_color_4->SetValue(m_param_color_count == 4); + if (m_btn_color_8) m_btn_color_8->SetValue(m_param_color_count == 8); + if (m_btn_color_16) m_btn_color_16->SetValue(m_param_color_count == 16); +} + +void TextureImportDialog::set_color_count_value(int value, bool update_spin) +{ + m_param_color_count = std::clamp(value, 1, (int)max_filament_count()); + m_color_slider->SetValue(m_param_color_count); + if (update_spin) + m_color_spin->SetValue(m_param_color_count); + update_color_count_preset_buttons(); + update_confirm_button_state(); +} + +void TextureImportDialog::set_smooth_value(int value, bool update_spin) +{ + m_param_smooth = std::clamp(value, 0, 10); + m_smooth_slider->SetValue(m_param_smooth); + if (update_spin) + m_smooth_spin->SetValue(m_param_smooth); + update_confirm_button_state(); +} + +void TextureImportDialog::preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param, + int min_value, int max_value, const wxString& text, + std::function on_value_changed) +{ + long value; + if (!text.ToLong(&value)) + return; + + wxTextCtrl* tc = spin->GetTextCtrl(); + long parsed = value; + value = std::clamp((int)parsed, min_value, max_value); + + wxString normalized = text; + if (parsed > max_value || (text.length() > 1 && text[0] == '0')) + normalized = wxString::Format("%ld", value); + + if (normalized != text) { + long pos = tc->GetInsertionPoint(); + tc->ChangeValue(normalized); + if (parsed > max_value) + tc->SetInsertionPointEnd(); + else + tc->SetInsertionPoint(std::min(normalized.length(), std::max(0L, pos - 1))); + } + + param = (int)value; + slider->SetValue(param); + if (on_value_changed) + on_value_changed(); + update_confirm_button_state(); +} + +void TextureImportDialog::on_color_preset_clicked(wxCommandEvent& evt) +{ + int id = evt.GetId(); + int color_count = m_param_color_count; + if (id == ID_COLOR_4) { color_count = 4; } + if (id == ID_COLOR_8) { color_count = 8; } + if (id == ID_COLOR_16) { color_count = 16; } + + if (id == ID_COLOR_AUTO) { + start_computation(true); + return; + } + + set_color_count_value(color_count, true); +} + +void TextureImportDialog::on_color_slider_changed(wxCommandEvent&) +{ + set_color_count_value(m_color_slider->GetValue(), true); +} + +void TextureImportDialog::on_color_spin_changed(wxCommandEvent&) +{ + set_color_count_value(m_color_spin->GetValue(), true); +} + +void TextureImportDialog::on_color_spin_text_changed(wxCommandEvent& evt) +{ + preview_spin_text_value(m_color_spin, m_color_slider, m_param_color_count, + 1, (int)max_filament_count(), evt.GetString(), + [this]() { update_color_count_preset_buttons(); }); +} + +void TextureImportDialog::on_smooth_slider_changed(wxCommandEvent&) +{ + set_smooth_value(m_smooth_slider->GetValue(), true); +} + +void TextureImportDialog::on_smooth_spin_changed(wxCommandEvent&) +{ + set_smooth_value(m_smooth_spin->GetValue(), true); +} + +void TextureImportDialog::on_smooth_spin_text_changed(wxCommandEvent& evt) +{ + preview_spin_text_value(m_smooth_spin, m_smooth_slider, m_param_smooth, + 0, 10, evt.GetString()); +} + +void TextureImportDialog::on_apply_clicked(wxCommandEvent&) +{ + start_computation(); +} + +void TextureImportDialog::on_auto_merge_toggled(wxCommandEvent&) +{ + bool auto_merge_enabled = !m_auto_merge_cb || m_auto_merge_cb->GetValue(); + m_auto_merge_enabled = auto_merge_enabled; + + if (m_state == TextureImportState::Ready) { + const auto previous_matches = m_current_matches; + do_auto_match(); + restore_current_match_order(previous_matches); + compact_used_virtual_filaments(); + update_filament_color_map(); + rebuild_mapping_rows(); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); + } +} + +void TextureImportDialog::highlight_view_button(int view_index) +{ + Button* btns[] = { m_btn_view_original, m_btn_view_multicolor }; + + // The inactive pill lies on m_tab_panel, which is preview_bg (#EEEEEE -> #4C4C55), and has to + // read as raised above that strip in both themes — so its fill steps away from the strip in + // opposite directions. gDarkColors pairs one light tone with one dark tone and cannot express + // an inversion, so the two are picked here the way filament_swatch_border_colour() does. + const bool dark_pill = is_dark(); + StateColor inactive_bg( + std::pair(dark_pill ? wxColour(0x5C, 0x5C, 0x64) : wxColour("#F4F4F4"), StateColor::Pressed), + std::pair(dark_pill ? wxColour(0x66, 0x66, 0x6E) : wxColour("#F8F8F8"), StateColor::Hovered), + std::pair(dark_pill ? wxColour(0x54, 0x54, 0x5B) : *wxWHITE, StateColor::Normal)); + const wxColour inactive_bd = dark_pill ? wxColour(0x54, 0x54, 0x5B) : *wxWHITE; + const wxColour inactive_text = wxColour("#6B6B6A"); + + for (int i = 0; i < 2; ++i) { + if (!btns[i]) continue; + if (i == view_index) { + apply_accent_button_colours(btns[i]); + } else { + btns[i]->SetBackgroundColor(inactive_bg); + btns[i]->SetBorderColor(inactive_bd); + btns[i]->SetTextColor(inactive_text); + } + btns[i]->Refresh(); + } +} + +void TextureImportDialog::on_skip_clicked(wxCommandEvent&) +{ + m_skipped = true; + m_new_filament_colors.clear(); + m_new_filament_preset_names.clear(); + m_new_mixed_filaments.clear(); + m_current_matches.clear(); + cancel_computation(); + EndModal(wxID_CANCEL); +} + +bool TextureImportDialog::has_valid_result() const +{ + if (m_painted.face_colors.empty() || m_current_matches.empty() || m_mapping_rows.empty()) + return false; + + if (m_mapping_rows.size() != m_current_matches.size()) + return false; + + const int filament_count = (int)std::min(m_filament_colors_rgba.size(), max_filament_count()); + for (const auto& row : m_mapping_rows) { + if (row.target_filament_idx < 0 || row.target_filament_idx >= filament_count) + return false; + } + return true; +} + +bool TextureImportDialog::is_params_dirty() const +{ + if (m_applied_color_count < 0) + return false; + return m_param_color_count != m_applied_color_count + || m_param_smooth != m_applied_smooth; +} + +void TextureImportDialog::update_drop_warning_visibility() +{ + if (!m_drop_warning_label) return; + // Show only when the most recent do_auto_match() ran into the filament + // cap AND we are in the Ready state. The flag is reset at every + // do_auto_match() entry, so any "clean" re-run automatically hides the + // warning even if a previous run had dropped clusters. + const bool show = (m_state == TextureImportState::Ready) && m_filaments_dropped; + if (m_drop_warning_label->IsShown() == show) return; + m_drop_warning_label->Show(show); + Layout(); +} + +void TextureImportDialog::update_confirm_button_state() +{ + if (m_state != TextureImportState::Ready) + return; + + if (!has_valid_result()) { + m_btn_ok->Enable(false); + if (m_hint_label) m_hint_label->Hide(); + m_btn_ok->Refresh(); + Layout(); + return; + } + + m_btn_ok->Enable(true); + style_confirm_button(is_params_dirty()); + + m_btn_ok->Refresh(); + Layout(); +} + +// Both state updaters land here: the Confirm button reads as accent only while it would apply +// exactly what the preview shows. +void TextureImportDialog::style_confirm_button(bool dirty) +{ + if (dirty) { + apply_muted_button_colours(m_btn_ok); + m_btn_ok->SetToolTip(_L("Reminder: parameters changed, click Apply to take effect")); + } else { + apply_accent_button_colours(m_btn_ok); + m_btn_ok->UnsetToolTip(); + } + if (m_hint_label) + m_hint_label->Show(dirty); +} + +void TextureImportDialog::on_ok_clicked(wxCommandEvent&) +{ + if (m_state != TextureImportState::Ready || !has_valid_result() || is_params_dirty()) + return; + + m_current_matches = build_matches_from_rows(); + if (m_current_matches.empty()) + return; + + compact_used_virtual_filaments(); + + EndModal(wxID_OK); +} + +// ---- Result accessors ---- + +Slic3r::PaintedMesh TextureImportDialog::get_painted_mesh() const +{ + return m_painted; +} + +std::vector TextureImportDialog::get_matches() const +{ + if (!m_current_matches.empty()) + return m_current_matches; + return build_matches_from_rows(); +} + +void TextureImportDialog::on_dpi_changed(const wxRect&) +{ + // All control sizes below are baked into persistent properties (min size, + // corner radius, fixed wxSize) using FromDIP() at build time. The base + // DPIAware::rescale() only rescales fonts; it does not recompute these + // stored pixel values. Re-apply them here so the layout stays consistent + // when the dialog is dragged to a screen with a different DPI. + SetMinSize(wxSize(FromDIP(800), FromDIP(500))); + + const int view_button_height = FromDIP(27); + for (Button* btn : {m_btn_view_original, m_btn_view_multicolor}) { + if (btn) { + btn->SetCornerRadius(view_button_height / 2); + btn->SetMinSize(wxSize(FromDIP(57), view_button_height)); + } + } + + for (Button* btn : {m_btn_color_4, m_btn_color_8, m_btn_color_16}) { + if (btn) { + btn->SetCornerRadius(FromDIP(12)); + btn->SetMinSize(wxSize(FromDIP(28), FromDIP(28))); + } + } + + if (m_btn_color_auto) { + m_btn_color_auto->SetCornerRadius(FromDIP(12)); + m_btn_color_auto->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + } + if (m_btn_apply) { + m_btn_apply->SetCornerRadius(FromDIP(12)); + m_btn_apply->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + } + if (m_btn_auto_mix) { + m_btn_auto_mix->SetCornerRadius(FromDIP(14)); + m_btn_auto_mix->SetMinSize(wxSize(FromDIP(178), FromDIP(28))); + } + if (m_btn_mix_reset) + m_btn_mix_reset->SetPaddingSize(wxSize(FromDIP(2), FromDIP(2))); + + if (m_color_spin) + m_color_spin->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + if (m_smooth_spin) + m_smooth_spin->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + + if (m_mapping_scroll) { + m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300))); + m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + } + + if (m_btn_skip) { + m_btn_skip->SetCornerRadius(FromDIP(20)); + m_btn_skip->SetMinSize(wxSize(FromDIP(136), FromDIP(40))); + } + if (m_btn_ok) { + m_btn_ok->SetCornerRadius(FromDIP(20)); + m_btn_ok->SetMinSize(wxSize(FromDIP(156), FromDIP(40))); + } + + // Mapping rows store their panel sizes (source/target/arrow/row height) + // as fixed FromDIP min/max sizes, so rebuild them to pick up the new DPI. + rebuild_mapping_rows(); + + if (wxSizer* sizer = GetSizer()) + sizer->Layout(); + Layout(); + Refresh(); + wxGetApp().UpdateDlgDarkUI(this); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/TextureImportDialog.hpp b/src/slic3r/GUI/TextureImportDialog.hpp new file mode 100644 index 0000000000..960bac6145 --- /dev/null +++ b/src/slic3r/GUI/TextureImportDialog.hpp @@ -0,0 +1,407 @@ +#pragma once + +#include "GUI_Utils.hpp" +#include "Widgets/ProgressDialog.hpp" +#include "libslic3r/TexturePainting.hpp" + +#include +#include +#include "Widgets/PopupWindow.hpp" +#include +#include +#include +#include "Widgets/SpinInput.hpp" +#include +#include +#include "Widgets/Button.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class AccentSlider; + +namespace Slic3r { namespace GUI { + +wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent); +wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent); +wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent); +wxDECLARE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent); + +enum class TextureImportState { + Idle, + Computing, + Ready, + Error +}; + +enum class TextureAutoMixMode { + CMYW, + RYBW +}; + +enum class TextureFilamentKind { + ExistingPhysical, + ExistingMixed, + NewPhysical, + NewMixed +}; + +struct TextureFilamentEntry { + TextureFilamentKind kind{TextureFilamentKind::ExistingPhysical}; + int dialog_index{-1}; + size_t project_config_index{size_t(-1)}; + std::string color_hex; + std::string name; + std::string type; + std::string preset_name; + std::vector mixed_components; + std::vector mixed_ratios; +}; + +struct TextureNewMixedFilament { + int dialog_index{-1}; + std::string color_hex; + std::vector component_dialog_indices; + std::vector ratios; +}; + +struct FilamentMappingRow { + int cluster_id = -1; + std::array source_color = {0, 0, 0}; + std::string source_hex; + int target_filament_idx = 0; + wxPanel* source_panel = nullptr; + wxPanel* target_panel = nullptr; +}; + +class FilamentSelectPopup; +class AutoMixSelectPopup; +// Lightweight 3D preview panel using wxGLCanvas. +// Renders: original textured, multi-color, or filament-mapped. +class TexturePreviewCanvas : public wxGLCanvas +{ +public: + enum class RenderMode { Original, MultiColor, FilamentMap }; + + TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs); + ~TexturePreviewCanvas(); + + void set_mesh_data( + const std::vector>& vertices, + const std::vector>& indices); + + void set_texture_data( + const std::vector>& uvs, + const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels); + + void set_texture_render_data( + const std::vector>& tex_pixels_rgb, + const std::vector& tex_widths, + const std::vector& tex_heights, + const std::vector, 3>>& face_uvs, + const std::vector& face_tex_ids); + + void set_painted_mesh_data( + const std::vector>& vertices, + const std::vector>& indices); + void set_face_colors(const std::vector>& face_colors); + void set_original_face_colors(const std::vector>& face_colors); + void set_filament_color_map(const std::map, std::array>& color_map); + + void set_render_mode(RenderMode mode); + RenderMode get_render_mode() const { return m_mode; } + void set_computing_overlay(bool show); + void reset_view(); + +private: + void on_paint(wxPaintEvent& evt); + void on_size(wxSizeEvent& evt); + void on_mouse(wxMouseEvent& evt); + void ensure_gl_ready(); + void render(); + void render_mesh(); + void render_textured_original(); + void render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size); + void upload_reset_icon_textures(); + unsigned int upload_reset_icon_texture(const std::string& icon_name); + wxRect reset_overlay_rect() const; + bool handle_reset_overlay_mouse(wxMouseEvent& evt); + void upload_textures(); + void compute_smooth_normals(); + void update_bounding_box(); + + wxGLContext* m_context = nullptr; + bool m_gl_initialized = false; + RenderMode m_mode = RenderMode::Original; + + float m_zoom = 1.0f; + float m_rot_x = -30.0f; + float m_rot_y = 30.0f; + float m_pan_x = 0.0f; + float m_pan_y = 0.0f; + wxPoint m_last_mouse_pos; + enum class DragMode { None, Rotate, Pan }; + DragMode m_drag_mode = DragMode::None; + + std::vector> m_vertices; + std::vector> m_indices; + std::vector> m_uvs; + std::vector> m_painted_vertices; + std::vector> m_painted_indices; + std::vector> m_face_colors_rgb; + std::vector> m_original_face_colors_rgb; + std::vector> m_filament_colors_rgb; + std::map, std::array> m_color_map; + + unsigned int m_tex_id = 0; + int m_tex_w = 0; + int m_tex_h = 0; + int m_tex_channels = 3; + bool m_tex_dirty = false; + std::vector m_tex_data; + + std::vector m_gl_tex_ids; + std::vector> m_tex_pixels_rgb; + std::vector m_tex_widths; + std::vector m_tex_heights; + std::vector, 3>> m_face_uvs; + std::vector m_face_tex_ids; + bool m_multi_tex_dirty = false; + + std::vector> m_vertex_normals; + + std::array m_center = {0, 0, 0}; + float m_radius = 1.0f; + + unsigned int m_reset_icon_tex = 0; + unsigned int m_reset_icon_hover_tex = 0; + unsigned int m_reset_icon_dark_tex = 0; + unsigned int m_reset_icon_dark_hover_tex = 0; + bool m_reset_overlay_hovered = false; + bool m_reset_overlay_pressed = false; +}; + + +class TextureImportDialog : public DPIDialog +{ +public: + TextureImportDialog(wxWindow* parent, + const Slic3r::TexturedMesh& textured_mesh, + const std::vector& filament_entries, + std::function initial_cancel_callback = {}, + std::function initial_progress_callback = {}); + ~TextureImportDialog(); + + int ShowModal() override; + void on_dpi_changed(const wxRect& suggested_rect) override; + + Slic3r::PaintedMesh get_painted_mesh() const; + std::vector get_matches() const; + bool was_skipped() const { return m_skipped; } + bool fallback_to_geometry_only() const { return m_fallback_to_geometry_only; } + // Colors of virtual filaments that need to be created after dialog confirmation. + // Index i corresponds to filament index (m_existing_filament_count + i). + const std::vector>& get_new_filament_colors() const { return m_new_filament_colors; } + const std::vector& get_new_filament_preset_names() const { return m_new_filament_preset_names; } + const std::vector& get_new_mixed_filaments() const { return m_new_mixed_filaments; } + const std::vector& get_filament_entries() const { return m_filament_entries; } + size_t get_existing_filament_count() const { return m_existing_filament_count; } + +private: + void build_ui(); + void build_preview_panel(wxWindow* parent, wxSizer* sizer); + void build_params_panel(wxWindow* parent, wxSizer* sizer); + void build_mapping_panel(wxWindow* parent, wxSizer* sizer); + void build_bottom_buttons(wxSizer* sizer); + + void set_state(TextureImportState new_state); + void update_ui_for_state(); + + void start_computation(bool auto_color = false, bool initial = false); + void cancel_computation(); + void on_computation_complete(wxCommandEvent& evt); + void on_computation_progress(wxCommandEvent& evt); + void on_computation_error(wxCommandEvent& evt); + void on_mesh_repair_decision_required(wxCommandEvent& evt); + + void rebuild_mapping_rows(); + void do_auto_match(); + // Reorder m_current_matches into a canonical, predictable order (ascending + // filament_index, with unmapped entries pushed to the end). Used right + // after the initial computation so the first view the user sees has a + // stable, intuitive layout. + void sort_current_matches_by_filament_index(); + // Reorder m_current_matches so they appear in the same order as + // `previous_matches` (keyed by cluster_index). Entries whose cluster_index + // was not present before are appended at the end, preserving their current + // relative order. Used when the user toggles auto-merge so the rows do not + // visually jump around. Assumes each cluster_index appears at most once in + // both vectors (this invariant is currently guaranteed by do_auto_match, + // which produces one match per cluster). + void restore_current_match_order(const std::vector& previous_matches); + std::vector build_matches_from_rows() const; + void update_filament_color_map(); + void show_filament_popup(size_t row_index); + void dismiss_filament_popup(); + void dismiss_filament_popup_on_wheel(wxMouseEvent& evt); + void show_auto_mix_popup(); + void dismiss_auto_mix_popup(); + void set_auto_mix_mode(TextureAutoMixMode mode); + void apply_auto_standard_mix(TextureAutoMixMode mode); + void reset_auto_mix(); + void update_auto_mix_reset_visibility(); + bool add_decomposed_mixed_filament(size_t row_index); + int add_virtual_filament(const std::array& rgba, const std::string& hex, + const std::string& preset_name = std::string()); + int add_virtual_mixed_filament(const std::string& color_hex, + const std::vector& component_dialog_indices, + const std::vector& ratios); + size_t max_filament_count() const; + bool can_add_virtual_filament() const; + // Recomputes m_drop_warning_label visibility from m_filaments_dropped and + // m_state. Safe to call whether or not the label has been created yet. + // Visibility reflects ONLY the result of the most recent do_auto_match(): + // if the latest match did not drop any cluster, the label is hidden even + // if a previous match had dropped (no historical accumulation). + void update_drop_warning_visibility(); + void compact_used_virtual_filaments(); + int find_closest_filament_index(const std::array& color) const; + // Returns a vector indexed by dialog_index whose value is the 1-based + // display number that mirrors the final sidebar ordering produced by + // apply_textured_mesh_import_result (Plater.cpp): ExistingPhysical, + // NewPhysical, ExistingMixed, NewMixed. Used so the dialog shows the + // same IDs the sidebar will show after OK, instead of the raw + // dialog_index + 1 (which interleaves physicals and mixeds). + // MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896). + std::vector compute_display_numbers() const; + + void on_color_preset_clicked(wxCommandEvent& evt); + void on_color_slider_changed(wxCommandEvent& evt); + void on_color_spin_changed(wxCommandEvent& evt); + void on_color_spin_text_changed(wxCommandEvent& evt); + void on_smooth_slider_changed(wxCommandEvent& evt); + void on_smooth_spin_changed(wxCommandEvent& evt); + void on_smooth_spin_text_changed(wxCommandEvent& evt); + void on_apply_clicked(wxCommandEvent& evt); + void on_auto_merge_toggled(wxCommandEvent& evt); + void highlight_view_button(int view_index); + void on_skip_clicked(wxCommandEvent& evt); + void on_ok_clicked(wxCommandEvent& evt); + + void set_color_count_value(int value, bool update_spin); + void set_smooth_value(int value, bool update_spin); + void preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param, + int min_value, int max_value, const wxString& text, + std::function on_value_changed = {}); + void update_color_count_preset_buttons(); + + bool has_valid_result() const; + bool is_params_dirty() const; + void update_confirm_button_state(); + void style_confirm_button(bool dirty); + + Slic3r::TexturedMesh m_textured_mesh; + std::vector m_filament_color_strs; // existing + virtual + std::vector m_filament_names; // existing + virtual + std::vector> m_filament_colors_rgba; // existing + virtual + std::vector m_filament_entries; // aligned with m_filament_colors_rgba + size_t m_existing_filament_count = 0; + std::vector> m_new_filament_colors; // only virtual (to be created) + std::vector m_new_filament_preset_names; // only virtual, aligned with m_new_filament_colors + std::vector m_new_mixed_filaments; + std::string m_default_virtual_filament_preset_name; + + TextureImportState m_state = TextureImportState::Idle; + bool m_skipped = false; + bool m_fallback_to_geometry_only = false; + // True iff *the most recent* do_auto_match() ran into the global filament + // limit and had to drop one or more clusters. Reset to false on every + // do_auto_match() entry so it never accumulates across runs: a run that + // does not drop anything must observe false here, regardless of whether + // previous runs dropped. Drives the inline orange warning above the + // bottom buttons; never affects the mapping itself. + bool m_filaments_dropped = false; + bool m_auto_merge_enabled = true; + TextureAutoMixMode m_auto_mix_mode = TextureAutoMixMode::CMYW; + int m_auto_mix_font_point_size = 10; + + Slic3r::PaintedMesh m_painted; + std::vector m_current_matches; + + std::unique_ptr m_worker; + std::atomic m_cancel_flag{false}; + std::mutex m_result_mutex; + Slic3r::PaintedMesh m_pending_result; + std::function m_initial_cancel_callback; + std::function m_initial_progress_callback; + bool m_current_computation_initial = false; + bool m_initial_computation_pending = false; + bool m_initial_computation_cancelled = false; + bool m_initial_computation_failed = false; + bool m_initial_tooltips_set = false; + bool m_current_computation_auto_color = false; + Slic3r::TexturePaintingSettings::MeshRepairDecision m_mesh_repair_decision = + Slic3r::TexturePaintingSettings::MeshRepairDecision::Ask; + + Button* m_btn_color_4 = nullptr; + Button* m_btn_color_8 = nullptr; + Button* m_btn_color_16 = nullptr; + Button* m_btn_color_auto = nullptr; + AccentSlider* m_color_slider = nullptr; + SpinInput* m_color_spin = nullptr; + AccentSlider* m_smooth_slider = nullptr; + SpinInput* m_smooth_spin = nullptr; + Button* m_btn_apply = nullptr; + + wxCheckBox* m_auto_merge_cb = nullptr; + Button* m_btn_auto_mix = nullptr; + Button* m_btn_mix_reset = nullptr; + bool m_auto_mix_applied = false; + AutoMixSelectPopup* m_auto_mix_popup = nullptr; + wxScrolledWindow* m_mapping_scroll = nullptr; + wxBoxSizer* m_mapping_sizer = nullptr; + std::vector m_mapping_rows; + FilamentSelectPopup* m_filament_popup = nullptr; + int m_filament_popup_row = -1; + int m_skip_next_filament_popup_row = -1; + + TexturePreviewCanvas* m_preview_canvas = nullptr; + wxPanel* m_tab_panel = nullptr; + Button* m_btn_view_original = nullptr; + Button* m_btn_view_multicolor = nullptr; + + ProgressDialog* m_progress_dlg = nullptr; + + Button* m_btn_skip = nullptr; + Button* m_btn_ok = nullptr; + wxStaticText* m_drop_warning_label = nullptr; + + int m_param_color_count = 4; + int m_param_smooth = 5; + + int m_applied_color_count = -1; + int m_applied_smooth = -1; + wxStaticText* m_hint_label = nullptr; + + static const int ID_COLOR_4 = wxID_HIGHEST + 200; + static const int ID_COLOR_8 = wxID_HIGHEST + 201; + static const int ID_COLOR_16 = wxID_HIGHEST + 202; + static const int ID_COLOR_AUTO = wxID_HIGHEST + 203; + static const int ID_BTN_APPLY = wxID_HIGHEST + 204; + static const int ID_BTN_SKIP = wxID_HIGHEST + 205; + static const int ID_VIEW_ORIGINAL = wxID_HIGHEST + 206; + static const int ID_VIEW_MULTICOLOR = wxID_HIGHEST + 207; + + wxDECLARE_EVENT_TABLE(); +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index b25e852c6b..fc6b8043f4 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -343,7 +343,7 @@ public: UnsavedChangesDialog(const wxString &caption, const wxString &header, DynamicConfig *config, int from, int to, bool left_to_right, NozzleVolumeType nozzle); ~UnsavedChangesDialog() override = default; - int ShowModal(); + int ShowModal() override; void build(Preset::Type type, PresetCollection *dependent_presets, const std::string &new_selected_preset, const wxString &header = ""); void update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header); diff --git a/src/slic3r/GUI/Widgets/ComboBox.cpp b/src/slic3r/GUI/Widgets/ComboBox.cpp index 783e1caadf..b6f6d42450 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.cpp +++ b/src/slic3r/GUI/Widgets/ComboBox.cpp @@ -87,10 +87,18 @@ void ComboBox::SetSelection(int n) return; drop.SetSelection(n); SetLabel(drop.GetValue()); - if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) - SetIcon(items[drop.selection].icon_textctrl); - else + if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) { + if (m_keep_drop_arrow) { + SetIcon("drop_down"); + SetIcon_1(items[drop.selection].icon_textctrl); + } else { + SetIcon(items[drop.selection].icon_textctrl); + } + } else { SetIcon("drop_down"); + if (m_keep_drop_arrow) + SetIcon_1(wxNullBitmap); + } if (drop.selection >= 0) { SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap); @@ -120,10 +128,18 @@ void ComboBox::SetValue(const wxString &value) { drop.SetValue(value); SetLabel(value); - if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) - SetIcon(items[drop.selection].icon_textctrl); - else + if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) { + if (m_keep_drop_arrow) { + SetIcon("drop_down"); + SetIcon_1(items[drop.selection].icon_textctrl); + } else { + SetIcon(items[drop.selection].icon_textctrl); + } + } else { SetIcon("drop_down"); + if (m_keep_drop_arrow) + SetIcon_1(wxNullBitmap); + } if (drop.selection >= 0) { SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap); diff --git a/src/slic3r/GUI/Widgets/ComboBox.hpp b/src/slic3r/GUI/Widgets/ComboBox.hpp index 552909b477..91c34d53aa 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.hpp +++ b/src/slic3r/GUI/Widgets/ComboBox.hpp @@ -16,6 +16,7 @@ class ComboBox : public wxWindowWithItems bool drop_down = false; bool text_off = false; bool is_replace_text_to_image = false; + bool m_keep_drop_arrow = false; // When true, item icon goes to icon_1, keeping drop_down arrow wxString replace_text; wxString image_for_text; @@ -31,6 +32,11 @@ public: DropDown & GetDropDown() { return drop; } + // When true, item icon is shown as icon_1 (secondary), preserving drop_down arrow. + // Note: item bitmaps are set via raw wxBitmap (not ScalableBitmap), so they won't + // auto-rescale on DPI change. Caller should recreate items after DPI change. + void SetKeepDropArrow(bool keep) { m_keep_drop_arrow = keep; } + virtual bool SetFont(wxFont const & font) override; public: diff --git a/src/slic3r/GUI/Widgets/DropDown.cpp b/src/slic3r/GUI/Widgets/DropDown.cpp index 973113d0ac..cd4d5edff8 100644 --- a/src/slic3r/GUI/Widgets/DropDown.cpp +++ b/src/slic3r/GUI/Widgets/DropDown.cpp @@ -427,7 +427,10 @@ void DropDown::render(wxDC &dc) } pt.y += (rcContent.height - textSize.y) / 2; dc.SetFont(GetFont()); - dc.SetTextForeground(text_color.colorForStates(states2)); + // Dimmed items stay selectable, so they only borrow the disabled text tone rather + // than taking the disabled state itself. + const int text_states = (item.style & DD_ITEM_STYLE_DIMMED) ? (states2 & ~StateColor::Enabled) : states2; + dc.SetTextForeground(text_color.colorForStates(text_states)); dc.DrawText(text, pt); if (group.IsEmpty() && !item.group_key.IsEmpty()) { auto szBmp = arrow_bitmap.GetBmpSize(); diff --git a/src/slic3r/GUI/Widgets/DropDown.hpp b/src/slic3r/GUI/Widgets/DropDown.hpp index 09041e3dc0..bcd0a58c41 100644 --- a/src/slic3r/GUI/Widgets/DropDown.hpp +++ b/src/slic3r/GUI/Widgets/DropDown.hpp @@ -13,6 +13,7 @@ #define DD_ITEM_STYLE_SPLIT_ITEM 0x0001 // ----text----, text with horizontal line arounds #define DD_ITEM_STYLE_DISABLED 0x0002 // ----text----, text with horizontal line arounds +#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent); diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 05857c6d0d..518bda1d19 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -630,7 +630,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD SetSizer(sizer); Layout(); - m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this,sizer](wxWebViewEvent& evt) { + m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { std::string message = evt.GetString().ToStdString(); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << "Received message: " << message; try { @@ -1168,8 +1168,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr rack, bool m_cancel_btn->SetLabel(_L("Ignore")); m_confirm_btn->SetLabel(_L("Refresh")); - m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); }); - m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, ignore_opt](auto& e) {ignore_opt(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); }); } else if (has_unknown) { m_cancel_btn->Show(); @@ -1178,8 +1178,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr rack, bool m_cancel_btn->SetLabel(_L("Ignore")); m_confirm_btn->SetLabel(_L("Refresh")); - m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); }); - m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, ignore_opt](auto& e) {ignore_opt(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); }); } else if (has_unreliable) { m_cancel_btn->Show(); @@ -1188,8 +1188,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr rack, bool m_cancel_btn->SetLabel(_L("Refresh")); m_confirm_btn->SetLabel(_L("Confirm")); - m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); - m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, trust_cmd](auto& e) {trust_cmd(); }); + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, trust_cmd](auto& e) {trust_cmd(); }); } else { diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp index ab56663928..3af524c2fc 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -167,7 +167,7 @@ class MultiNozzleSyncDialog : public DPIDialog { public: MultiNozzleSyncDialog(wxWindow* parent, std::weak_ptr rack); - virtual void on_dpi_changed(const wxRect& suggested_rect) {}; + virtual void on_dpi_changed(const wxRect& suggested_rect) override {}; std::vector GetNozzleOptions(const std::vector& group_infos); std::optional GetSelectedOption() { diff --git a/src/slic3r/GUI/Widgets/ProgressBar.hpp b/src/slic3r/GUI/Widgets/ProgressBar.hpp index 38dda6c8d2..40ddb8e4be 100644 --- a/src/slic3r/GUI/Widgets/ProgressBar.hpp +++ b/src/slic3r/GUI/Widgets/ProgressBar.hpp @@ -56,7 +56,7 @@ protected: void paintEvent(wxPaintEvent &evt); void render(wxDC &dc); void doRender(wxDC &dc); - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; diff --git a/src/slic3r/GUI/Widgets/ProgressDialog.hpp b/src/slic3r/GUI/Widgets/ProgressDialog.hpp index 597ec7f802..bb770298a9 100644 --- a/src/slic3r/GUI/Widgets/ProgressDialog.hpp +++ b/src/slic3r/GUI/Widgets/ProgressDialog.hpp @@ -33,7 +33,7 @@ public: void OnPaint(wxPaintEvent &evt); virtual ~ProgressDialog(); - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; bool Create(const wxString &title, const wxString &message, int maximum = 100, wxWindow *parent = NULL, int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE); virtual bool Update(int value, const wxString &newmsg = wxEmptyString, bool *skip = NULL); diff --git a/src/slic3r/GUI/Widgets/SideButton.hpp b/src/slic3r/GUI/Widgets/SideButton.hpp index 4f8d893f93..894a7d8727 100644 --- a/src/slic3r/GUI/Widgets/SideButton.hpp +++ b/src/slic3r/GUI/Widgets/SideButton.hpp @@ -31,7 +31,7 @@ public: void SetLayoutStyle(int style); - void SetLabel(const wxString& label); + void SetLabel(const wxString& label) override; bool SetForegroundColour(wxColour const & colour) override; @@ -47,7 +47,7 @@ public: void SetBackgroundColor(StateColor const &color); - bool Enable(bool enable = true); + bool Enable(bool enable = true) override; void Rescale(); diff --git a/src/slic3r/GUI/Widgets/SpinInput.cpp b/src/slic3r/GUI/Widgets/SpinInput.cpp index fba5a45233..c91fd41543 100644 --- a/src/slic3r/GUI/Widgets/SpinInput.cpp +++ b/src/slic3r/GUI/Widgets/SpinInput.cpp @@ -9,6 +9,8 @@ #include "../GUI_Utils.hpp" #endif +wxDEFINE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent); + BEGIN_EVENT_TABLE(SpinInput, StaticBox) EVT_KEY_DOWN(SpinInput::keyPressed) @@ -74,8 +76,9 @@ void SpinInput::Create(wxWindow *parent, state_handler.attach_child(text_ctrl); text_ctrl->Bind(wxEVT_KILL_FOCUS, &SpinInput::onTextLostFocus, this); text_ctrl->Bind(wxEVT_TEXT_ENTER, &SpinInput::onTextEnter, this); + text_ctrl->Bind(wxEVT_TEXT, &SpinInput::onTextChanged, this); text_ctrl->Bind(wxEVT_KEY_DOWN, &SpinInput::keyPressed, this); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu button_inc = createButton(true); button_dec = createButton(false); delta = 0; @@ -300,6 +303,19 @@ void SpinInput::onTextEnter(wxCommandEvent &event) ProcessEventLocally(event); } +void SpinInput::onTextChanged(wxCommandEvent &event) +{ + long value; + if (text_ctrl->GetValue().ToLong(&value)) { + wxCommandEvent e(EVT_SPINCTRL_TEXT, GetId()); + e.SetEventObject(this); + e.SetInt((int) value); + e.SetString(text_ctrl->GetValue()); + GetEventHandler()->ProcessEvent(e); + } + event.Skip(); +} + void SpinInput::mouseWheelMoved(wxMouseEvent &event) { auto delta = event.GetWheelRotation() < 0 ? 1 : -1; diff --git a/src/slic3r/GUI/Widgets/SpinInput.hpp b/src/slic3r/GUI/Widgets/SpinInput.hpp index 275d42a95d..caf2bf3843 100644 --- a/src/slic3r/GUI/Widgets/SpinInput.hpp +++ b/src/slic3r/GUI/Widgets/SpinInput.hpp @@ -9,6 +9,10 @@ class Button; +// Fired on every keystroke that leaves a parseable integer in the field, so callers can +// react live rather than only on commit (wxEVT_SPINCTRL) or Enter. Ported from BambuStudio. +wxDECLARE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent); + class SpinInput : public wxNavigationEnabled { wxSize labelSize; @@ -98,6 +102,7 @@ private: void keyPressed(wxKeyEvent& event); void onTimer(wxTimerEvent &evnet); void onTextLostFocus(wxEvent &event); + void onTextChanged(wxCommandEvent &event); void onTextEnter(wxCommandEvent &event); void sendSpinEvent(); diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index 5374a32a73..e6a990243e 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -68,7 +68,7 @@ public: int buttons_best_width() const; private: - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; #ifdef __WIN32__ WXLRESULT MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) override; diff --git a/src/slic3r/GUI/Widgets/TempInput.cpp b/src/slic3r/GUI/Widgets/TempInput.cpp index 6a9809252a..6705378dac 100644 --- a/src/slic3r/GUI/Widgets/TempInput.cpp +++ b/src/slic3r/GUI/Widgets/TempInput.cpp @@ -134,7 +134,7 @@ void TempInput::Create(wxWindow *parent, wxString text, wxString label, wxString } } }); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu text_ctrl->Bind(wxEVT_LEFT_DOWN, [this](auto &e) { if (m_read_only) { return; diff --git a/src/slic3r/GUI/Widgets/TempInput.hpp b/src/slic3r/GUI/Widgets/TempInput.hpp index c306ba59cc..f281a1ea6e 100644 --- a/src/slic3r/GUI/Widgets/TempInput.hpp +++ b/src/slic3r/GUI/Widgets/TempInput.hpp @@ -107,7 +107,7 @@ public: wxString GetTagTemp() { return text_ctrl->GetValue(); } wxString GetCurrTemp() { return GetLabel(); } int get_max_temp() { return max_temp; } - void SetLabel(const wxString &label); + void SetLabel(const wxString &label) override; void SetTextColor(StateColor const &color); @@ -128,7 +128,7 @@ public: void ReSetOnChanging() { m_on_changing = false; } protected: - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; void DoSetToolTipText(wxString const &tip) override; diff --git a/src/slic3r/GUI/Widgets/TextInput.cpp b/src/slic3r/GUI/Widgets/TextInput.cpp index 49605e048d..23f55d155c 100644 --- a/src/slic3r/GUI/Widgets/TextInput.cpp +++ b/src/slic3r/GUI/Widgets/TextInput.cpp @@ -85,7 +85,7 @@ void TextInput::Create(wxWindow * parent, e.SetId(GetId()); ProcessEventLocally(e); }); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu if (!icon.IsEmpty()) { this->icon = ScalableBitmap(this, icon.ToStdString(), 16); } @@ -139,6 +139,15 @@ void TextInput::SetIcon_1(const wxString &icon) { Rescale(); } +// Set icon_1 from a raw bitmap. Note: won't auto-rescale on DPI change +// since ScalableBitmap::name() will be empty. Caller should re-set after DPI change. +void TextInput::SetIcon_1(const wxBitmap &icon) { + this->icon_1 = ScalableBitmap(); + if (icon.IsOk()) + this->icon_1.bmp() = icon; + Rescale(); +} + void TextInput::SetLabelColor(StateColor const &color) { label_color = color; diff --git a/src/slic3r/GUI/Widgets/TextInput.hpp b/src/slic3r/GUI/Widgets/TextInput.hpp index 9aca7037c4..bbd38b0c00 100644 --- a/src/slic3r/GUI/Widgets/TextInput.hpp +++ b/src/slic3r/GUI/Widgets/TextInput.hpp @@ -46,7 +46,7 @@ public: // Only meant to be used by inspector, not public API int GetCornerRadius() const { return static_cast(radius); } - void SetLabel(const wxString& label); + void SetLabel(const wxString& label) override; void SetStaticTips(const wxString& tips, const wxBitmap& bitmap); @@ -54,6 +54,7 @@ public: void SetIcon(const wxString & icon); void SetIcon_1(const wxString &icon); + void SetIcon_1(const wxBitmap &icon); void SetLabelColor(StateColor const &color); @@ -73,7 +74,7 @@ protected: virtual void OnEdit() {} virtual void DoSetSize( - int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; void DoSetToolTipText(wxString const &tip) override; diff --git a/src/slic3r/GUI/Widgets/WebView.cpp b/src/slic3r/GUI/Widgets/WebView.cpp index 36800dcf47..e281d97407 100644 --- a/src/slic3r/GUI/Widgets/WebView.cpp +++ b/src/slic3r/GUI/Widgets/WebView.cpp @@ -104,7 +104,7 @@ DWORD DownloadAndInstallWV2RT() { class WebViewEdge : public wxWebViewEdge { public: - bool SetUserAgent(const wxString &userAgent) + bool SetUserAgent(const wxString &userAgent) override { bool dark = userAgent.Contains("dark"); SetColorScheme(dark ? COREWEBVIEW2_PREFERRED_COLOR_SCHEME_DARK : COREWEBVIEW2_PREFERRED_COLOR_SCHEME_LIGHT); diff --git a/src/slic3r/GUI/WipeTowerDialog.cpp b/src/slic3r/GUI/WipeTowerDialog.cpp index abf8baf086..d4fbcc6fe3 100644 --- a/src/slic3r/GUI/WipeTowerDialog.cpp +++ b/src/slic3r/GUI/WipeTowerDialog.cpp @@ -204,6 +204,10 @@ bool is_flush_config_modified() const auto &project_config = wxGetApp().preset_bundle->project_config; const std::vector &config_matrix = (project_config.option("flush_volumes_matrix"))->values; const std::vector &config_multiplier = (project_config.option("flush_multiplier"))->values; + // The config matrix is N x N per nozzle over every slot, while CalcFlushingVolumes is p x p + // over the physical slots (mixed slots never flush): map each default cell to its config index. + const auto physical_indices = wxGetApp().preset_bundle->physical_filament_config_indices(); + const size_t full_n = project_config.option("filament_colour")->values.size(); bool has_modify = false; for (int i = 0; i < config_multiplier.size(); i++) { @@ -212,11 +216,12 @@ bool is_flush_config_modified() break; } std::vector> default_matrix = WipingDialog::CalcFlushingVolumes(i); - int len = default_matrix.size(); - for (int m = 0; m < len; m++) { - for (int n = 0; n < len; n++) { - int idx = i * len * len + m * len + n; - if (config_matrix[idx] != default_matrix[m][n] * config_multiplier[i]) { + size_t p_len = default_matrix.size(); + size_t nozzle_offset = i * full_n * full_n; + for (size_t m = 0; m < p_len; m++) { + for (size_t n = 0; n < p_len; n++) { + size_t cfg_idx = nozzle_offset + physical_indices[m] * full_n + physical_indices[n]; + if (cfg_idx < config_matrix.size() && config_matrix[cfg_idx] != default_matrix[m][n] * config_multiplier[i]) { has_modify = true; break; } @@ -256,6 +261,40 @@ static std::vector MatrixFlatten(const WipingDialog::VolumeMatrix& matrix return vec; } +// Mixed-color slots are virtual and have no flushing volumes, so the dialog shows only the +// physical filaments. That means converting between the full config matrix (indexed by config +// slot) and a dense physical sub-matrix (indexed by row/column in the table). +static std::vector extract_physical_sub_matrix( + const std::vector& full_matrix, size_t full_n, + const std::vector& indices) +{ + size_t p = indices.size(); + std::vector sub(p * p, 0.0); + if (full_matrix.size() < full_n * full_n) + return sub; + for (size_t pi = 0; pi < p; ++pi) + for (size_t pj = 0; pj < p; ++pj) + sub[pi * p + pj] = full_matrix[indices[pi] * full_n + indices[pj]]; + return sub; +} + +// Write the edited physical sub-matrix back into a copy of the full matrix, leaving the +// entries that belong to mixed slots untouched. +static std::vector expand_physical_to_full_matrix( + const std::vector& sub_matrix, + const std::vector& indices, size_t full_n, + const std::vector& original_matrix) +{ + std::vector full = original_matrix; + if (full.size() < full_n * full_n) + return full; + size_t p = indices.size(); + for (size_t pi = 0; pi < p; ++pi) + for (size_t pj = 0; pj < p; ++pj) + full[indices[pi] * full_n + indices[pj]] = sub_matrix[pi * p + pj]; + return full; +} + wxString WipingDialog::BuildTableObjStr() { auto full_config = wxGetApp().preset_bundle->full_config(); @@ -265,9 +304,22 @@ wxString WipingDialog::BuildTableObjStr() auto raw_matrix_data = full_config.option("flush_volumes_matrix")->values; auto nozzle_flush_dataset = full_config.option("nozzle_flush_dataset")->values; + // Restrict the table to physical filaments; mixed slots have no flushing volumes. + m_physical_indices = wxGetApp().preset_bundle->physical_filament_config_indices(); + const size_t full_n = filament_colors.size(); + { + std::vector physical_colors; + physical_colors.reserve(m_physical_indices.size()); + for (size_t i : m_physical_indices) + if (i < filament_colors.size()) + physical_colors.push_back(filament_colors[i]); + filament_colors = std::move(physical_colors); + } + std::vector> flush_matrixs; for (int idx = 0; idx < nozzle_num; ++idx) { - flush_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num)); + auto fm = get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num); + flush_matrixs.emplace_back(extract_physical_sub_matrix(fm, full_n, m_physical_indices)); } flush_multiplier.resize(nozzle_num, 1); @@ -372,7 +424,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) : wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); this->SetSizer(main_sizer); this->SetBackgroundColour(*wxWHITE); - auto filament_count = wxGetApp().preset_bundle->project_config.option("filament_colour")->values.size(); + auto filament_count = wxGetApp().preset_bundle->physical_filament_config_indices().size(); // Estimate table scroll area size based on filament count // Each table cell is ~60x25 DIP, plus headers and borders @@ -523,55 +575,51 @@ WipingDialog::VolumeMatrix WipingDialog::CalcFlushingVolumes(int extruder_id) auto& preset_bundle = wxGetApp().preset_bundle; auto full_config = preset_bundle->full_config(); auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; + // Mixed-colour slots are virtual and never flushed: compute a p x p matrix over the physical + // slots only, laid out like the table; row/column k belongs to config slot physical_indices[k]. + auto physical_indices = preset_bundle->physical_filament_config_indices(); - std::vector filament_color_strs = full_config.option("filament_colour")->values; - std::vector> multi_colors; - std::vector filament_colors; - for (auto color_str : filament_color_strs) - filament_colors.emplace_back(color_str); - + std::vector all_color_strs = full_config.option("filament_colour")->values; int flush_dataset_value = full_config.option("nozzle_flush_dataset")->values[extruder_id]; + const std::vector min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); + // Support for multi-color filament - for (int i = 0; i < filament_colors.size(); ++i) { + std::vector> multi_colors; + for (size_t cfg_idx : physical_indices) { std::vector single_filament; - if (i < ams_multi_color_filament.size()) { - if (!ams_multi_color_filament[i].empty()) { - std::vector colors = ams_multi_color_filament[i]; - for (int j = 0; j < colors.size(); ++j) { - single_filament.push_back(wxColour(colors[j])); - } - multi_colors.push_back(single_filament); - continue; - } + if (cfg_idx < ams_multi_color_filament.size() && !ams_multi_color_filament[cfg_idx].empty()) { + for (const auto& c : ams_multi_color_filament[cfg_idx]) + single_filament.push_back(wxColour(c)); + } else if (cfg_idx < all_color_strs.size()) { + single_filament.push_back(wxColour(all_color_strs[cfg_idx])); } - single_filament.push_back(wxColour(filament_colors[i])); multi_colors.push_back(single_filament); } VolumeMatrix matrix; - const std::vector min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); - - for (int from_idx = 0; from_idx < multi_colors.size(); ++from_idx) { - bool is_from_support = is_support_filament(from_idx); + for (size_t pi = 0; pi < physical_indices.size(); ++pi) { + int from_cfg = (int)physical_indices[pi]; + bool is_from_support = is_support_filament(from_cfg); matrix.emplace_back(); - for (int to_idx = 0; to_idx < multi_colors.size(); ++to_idx) { - if (from_idx == to_idx) { + for (size_t pj = 0; pj < physical_indices.size(); ++pj) { + int to_cfg = (int)physical_indices[pj]; + if (from_cfg == to_cfg) { matrix.back().emplace_back(0); continue; } - bool is_to_support = is_support_filament(to_idx); - + bool is_to_support = is_support_filament(to_cfg); int flushing_volume = 0; if (is_to_support) { flushing_volume = Slic3r::g_flush_volume_to_support; } else { - for (int i = 0; i < multi_colors[from_idx].size(); ++i) { - const wxColour& from = multi_colors[from_idx][i]; - for (int j = 0; j < multi_colors[to_idx].size(); ++j) { - const wxColour& to = multi_colors[to_idx][j]; - int volume = CalcFlushingVolume(from, to, min_flush_volumes[from_idx], flush_dataset_value); + int min_flush_from = (from_cfg < (int)min_flush_volumes.size()) ? min_flush_volumes[from_cfg] : 0; + for (size_t i = 0; i < multi_colors[pi].size(); ++i) { + const wxColour& from = multi_colors[pi][i]; + for (size_t j = 0; j < multi_colors[pj].size(); ++j) { + const wxColour& to = multi_colors[pj][j]; + int volume = CalcFlushingVolume(from, to, min_flush_from, flush_dataset_value); flushing_volume = std::max(flushing_volume, volume); } } @@ -592,11 +640,29 @@ void WipingDialog::StoreFlushData(int extruder_num, const std::vector WipingDialog::ExpandToFullMatrix(const std::vector& sub_matrix, int nozzle_idx) const +{ + const auto& project_config = wxGetApp().preset_bundle->project_config; + const size_t full_n = project_config.option("filament_colour")->values.size(); + if (m_physical_indices.size() == full_n) + return sub_matrix; // no mixed slots: sub-matrix already is the full matrix + + auto raw = project_config.option("flush_volumes_matrix")->values; + int nozzle_num = (int)wxGetApp().preset_bundle->project_config.option("flush_multiplier")->values.size(); + if (nozzle_num < 1) nozzle_num = 1; + auto original = get_flush_volumes_matrix(raw, nozzle_idx, nozzle_num); + return expand_physical_to_full_matrix(sub_matrix, m_physical_indices, full_n, original); +} + std::vector WipingDialog::GetFlattenMatrix()const { std::vector ret; - for (auto& matrix : m_raw_matrixs) { - ret.insert(ret.end(), matrix.begin(), matrix.end()); + for (size_t idx = 0; idx < m_raw_matrixs.size(); ++idx) { + auto full = ExpandToFullMatrix(m_raw_matrixs[idx], (int)idx); + ret.insert(ret.end(), full.begin(), full.end()); } return ret; } diff --git a/src/slic3r/GUI/WipeTowerDialog.hpp b/src/slic3r/GUI/WipeTowerDialog.hpp index 64e5758534..91944cfc78 100644 --- a/src/slic3r/GUI/WipeTowerDialog.hpp +++ b/src/slic3r/GUI/WipeTowerDialog.hpp @@ -58,12 +58,16 @@ private: wxString BuildTableObjStr(); wxString BuildTextObjStr(bool multi_language = true); void StoreFlushData(int extruder_num, const std::vector>& flush_volume_vecs, const std::vector& flush_multipliers); + // Maps the physical-only matrix shown in the table back onto the full config-indexed matrix. + std::vector ExpandToFullMatrix(const std::vector& sub_matrix, int nozzle_idx) const; wxWebView* m_webview; int m_max_flush_volume; VolumeMatrix m_raw_matrixs; std::vector m_flush_multipliers; + // Config indices of the physical (non-mixed) filaments, in table order. + std::vector m_physical_indices; bool m_submit_flag{ false }; }; diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 2ca8f3cfbd..88e2df31c0 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -555,14 +555,20 @@ std::vector get_extruder_color_icons(bool thin_icon/* = false*/) const int icon_width = lround((thin_icon ? 2 : 4.4) * em); const int icon_height = lround(2 * em); + // A gradient mixed filament fades over the model's height, so it gets the same + // curve-sampled ramp the editor previews instead of a fade between two endpoints. + const auto& gradient_ramps = Slic3r::GUI::wxGetApp().plater()->get_filament_gradient_ramps(); + int index = 0; for (const auto &colors : readable_color_info) { auto label = std::to_string(++index); - bool is_gradient = ctype[index-1] == "0"; - if (colors.size() == 1) { + const size_t slot = index - 1; + bool is_gradient = ctype[slot] == "0"; + const std::vector* ramp = (slot < gradient_ramps.size() && !gradient_ramps[slot].empty()) ? &gradient_ramps[slot] : nullptr; + if (ramp == nullptr && colors.size() == 1) { bmps.push_back(get_extruder_color_icon(colors[0], label, icon_width, icon_height)); } else { - bmps.push_back(get_extruder_color_icon(colors, is_gradient, label, icon_width, icon_height)); + bmps.push_back(get_extruder_color_icon(colors, is_gradient, label, icon_width, icon_height, ramp)); } } } else { @@ -630,14 +636,27 @@ wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_da return data; } -wxBitmap *get_extruder_color_icon(std::vector colors, bool is_gradient, std::string label, int icon_width, int icon_height){ +wxBitmap *get_extruder_color_icon(std::vector colors, bool is_gradient, std::string label, int icon_width, int icon_height, + const std::vector *ramp){ static Slic3r::GUI::BitmapCache bmp_cache; - // build cache key, include all color info + // build cache key, include all color info. A ramp already encodes its slot's components, + // colours and curve, so keying on it rebuilds the icon whenever any of them change. std::string bitmap_key = ""; - for (const auto& color : colors) { - bitmap_key += color + "_"; + if (ramp != nullptr) { + static const char hex_digits[] = "0123456789ABCDEF"; + bitmap_key = "grad_"; + for (const wxColour &c : *ramp) + for (unsigned char v : {c.Red(), c.Green(), c.Blue()}) { + bitmap_key += hex_digits[v >> 4]; + bitmap_key += hex_digits[v & 0x0F]; + } + bitmap_key += "_"; + } else { + for (const auto& color : colors) { + bitmap_key += color + "_"; + } } bitmap_key += "h" + std::to_string(icon_height) + "-w" + std::to_string(icon_width) + "-i" + label; @@ -647,16 +666,21 @@ wxBitmap *get_extruder_color_icon(std::vector colors, bool is_gradi #endif if (bitmap == nullptr) { - std::vector wx_colors; - for (const auto& color_str : colors) { - wx_colors.push_back(wxColour(color_str)); - } - if (wx_colors.empty()) { - wx_colors.push_back(wxColour("#636363")); // default color if no colors provided - } + wxBitmap base_bitmap; + if (ramp != nullptr) { + base_bitmap = Slic3r::GUI::create_gradient_ramp_bitmap(*ramp, wxSize(icon_width, icon_height)); + } else { + std::vector wx_colors; + for (const auto& color_str : colors) { + wx_colors.push_back(wxColour(color_str)); + } + if (wx_colors.empty()) { + wx_colors.push_back(wxColour("#636363")); // default color if no colors provided + } - // create filament bitmap in multi color - wxBitmap base_bitmap = Slic3r::GUI::create_filament_bitmap(wx_colors, wxSize(icon_width, icon_height), is_gradient); + // create filament bitmap in multi color + base_bitmap = Slic3r::GUI::create_filament_bitmap(wx_colors, wxSize(icon_width, icon_height), is_gradient); + } if (!base_bitmap.IsOk()) { // if create failed, return nullptr diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 502614eb92..2754b3e5ba 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -75,7 +75,10 @@ wxBitmap create_scaled_bitmap(const std::string& bmp_name, wxWindow *win = nullp wxBitmap* get_default_extruder_color_icon(bool thin_icon = false); std::vector get_extruder_color_icons(bool thin_icon = false); wxBitmap * get_extruder_color_icon(std::string color, std::string label, int icon_width, int icon_height); -wxBitmap * get_extruder_color_icon(std::vector colors, bool is_gradient, std::string label, int icon_width, int icon_height); +// A non-null ramp draws the slot as a gradient mixed filament instead: it holds the colours the +// slot actually prints, bottom entry first, and is drawn bottom to top rather than from colors. +wxBitmap * get_extruder_color_icon(std::vector colors, bool is_gradient, std::string label, int icon_width, int icon_height, + const std::vector *ramp = nullptr); std::vector> read_color_pack(std::vector color_pack); wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data); diff --git a/src/slic3r/Utils/CrealityPrint.hpp b/src/slic3r/Utils/CrealityPrint.hpp index ddb2054420..3b5287f382 100644 --- a/src/slic3r/Utils/CrealityPrint.hpp +++ b/src/slic3r/Utils/CrealityPrint.hpp @@ -21,14 +21,14 @@ public: ~CrealityPrint() override = default; const char* get_name() const override; - virtual bool can_test() const { return true; }; + virtual bool can_test() const override { return true; }; std::string get_host() const override; bool has_auto_discovery() const override { return true; } wxString get_test_ok_msg() const override; wxString get_test_failed_msg(wxString& msg) const override; virtual bool test(wxString& curl_msg) const override; - PrintHostPostUploadActions get_post_upload_actions() const; + PrintHostPostUploadActions get_post_upload_actions() const override; bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; bool supports_multi_color_print() const; std::string query_boxes_info() const; diff --git a/src/slic3r/Utils/ElegooLink.hpp b/src/slic3r/Utils/ElegooLink.hpp index eb1ca7ba26..a60d2de1b3 100644 --- a/src/slic3r/Utils/ElegooLink.hpp +++ b/src/slic3r/Utils/ElegooLink.hpp @@ -32,10 +32,10 @@ public: PrintHostPostUploadActions get_post_upload_actions() const override; protected: #ifdef WIN32 - virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const; + virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const override; #endif - virtual bool validate_version_text(const boost::optional &version_text) const; - virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; + virtual bool validate_version_text(const boost::optional &version_text) const override; + virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; #ifdef WIN32 virtual bool test_with_resolved_ip(wxString& curl_msg) const override; diff --git a/src/slic3r/Utils/Obico.hpp b/src/slic3r/Utils/Obico.hpp index 9fd3d50f6b..f262d204bd 100644 --- a/src/slic3r/Utils/Obico.hpp +++ b/src/slic3r/Utils/Obico.hpp @@ -20,7 +20,7 @@ public: ~Obico() override = default; const char* get_name() const override; - virtual bool can_test() const { return true; }; + virtual bool can_test() const override { return true; }; bool has_auto_discovery() const override { return false; } bool is_cloud() const override { return true; } bool get_login_url(wxString& auth_url) const override; @@ -30,7 +30,7 @@ public: wxString get_test_failed_msg(wxString& msg) const override; virtual bool test(wxString& curl_msg) const override; bool get_printers(wxArrayString& printers) const override; - PrintHostPostUploadActions get_post_upload_actions() const; + PrintHostPostUploadActions get_post_upload_actions() const override; bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; protected: diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 43afd4281d..fc46bb8fdc 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(${_TEST_NAME}_tests test_perimeters.cpp test_print.cpp test_printobject.cpp + test_mixed_filament.cpp test_skirt_brim.cpp test_slicing_pipeline_hook.cpp test_support_material.cpp diff --git a/tests/fff_print/test_mixed_filament.cpp b/tests/fff_print/test_mixed_filament.cpp new file mode 100644 index 0000000000..25b426c210 --- /dev/null +++ b/tests/fff_print/test_mixed_filament.cpp @@ -0,0 +1,324 @@ +#include + +#include "libslic3r/GCode/ToolOrdering.hpp" +#include "libslic3r/MultiNozzleUtils.hpp" +#include "libslic3r/Print.hpp" + +#include "test_helpers.hpp" + +using namespace Slic3r; +using namespace Slic3r::Test; + +namespace { + +// Two physical filaments plus one mixed slot (config index 2, 1-based id 3) blending them 60/40. +// The mixed arrays are parallel to filament_colour and must be sized to the filament count. +// Note ConfigOptionBools deserializes on ',' while ConfigOptionStrings uses ';'. +DynamicPrintConfig mixed_config(bool sublayer_on, const char *ratios = "0.6,0.4") +{ + DynamicPrintConfig config = multifilament_config(3); + config.set_deserialize_strict({ + {"filament_is_mixed", "0,0,1"}, + {"filament_mixed_components", ";;1,2"}, + {"filament_mixed_sublayer_ratios", std::string(";;") + ratios}, + {"filament_mixed_gradient", "0,0,0"}, + {"filament_mixed_gradient_range", ";;"}, + {"filament_mixed_gradient_curve", ";;"}, + {"filament_mixed_gradient_per_part","0,0,0"}, + {"enable_mixed_color_sublayer", sublayer_on ? "1" : "0"}, + // Assign every region role to the mixed slot so it actually participates in slicing. + {"outer_wall_filament_id", "3"}, + {"inner_wall_filament_id", "3"}, + {"sparse_infill_filament_id", "3"}, + {"internal_solid_filament_id", "3"}, + {"top_surface_filament_id", "3"}, + {"bottom_surface_filament_id", "3"}, + }); + return config; +} + +// Total sub-layer groups and per-layer mixed-filament resolutions across the whole tool ordering. +void count_mixed(ToolOrdering &to, size_t &groups, size_t &resolutions) +{ + groups = resolutions = 0; + for (const LayerTools < : to.layer_tools()) { + groups += lt.mixed_sub_layer_groups.size(); + resolutions += lt.mixed_filament_resolution.size(); + } +} + +} // namespace + +TEST_CASE("enable_mixed_color_sublayer reaches the Print config", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(true)); + + // The option lives in PrintConfig; if it did not survive Print::apply the slicer would + // silently fall back to the whole-layer path. + CHECK(print.config().enable_mixed_color_sublayer.value == true); + REQUIRE(print.config().filament_is_mixed.values.size() == 3); + CHECK(print.config().filament_is_mixed.values[2] == true); + REQUIRE(print.config().filament_mixed_components.values.size() == 3); + CHECK(print.config().filament_mixed_components.values[2] == "1,2"); +} + +TEST_CASE("Mixed filament splits layers into sub-layers when the option is on", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(true)); + print.process(); + + ToolOrdering &to = const_cast(print.tool_ordering()); + REQUIRE(!to.layer_tools().empty()); + + size_t groups = 0, resolutions = 0; + count_mixed(to, groups, resolutions); + + INFO("layers=" << to.layer_tools().size() << " groups=" << groups); + CHECK(groups > 0); +} + +TEST_CASE("Mixed filament alternates whole layers when the option is off", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(false)); + print.process(); + + ToolOrdering &to = const_cast(print.tool_ordering()); + REQUIRE(!to.layer_tools().empty()); + + size_t groups = 0, resolutions = 0; + count_mixed(to, groups, resolutions); + + // With splitting off the slot is realized by the deficit round-robin scheduler instead: + // no sub-layer groups, but a per-layer resolution to one physical component. + INFO("layers=" << to.layer_tools().size() << " resolutions=" << resolutions); + CHECK(groups == 0); + CHECK(resolutions > 0); +} + +TEST_CASE("Sub-layer splitting emits the scaled sub-heights into G-code", "[MixedFilament]") +{ + // layer_height 0.2 split 60/40 gives sub-layers of 0.12 and 0.08. The emitter reports the + // sub-height (not the nominal layer height) in the HEIGHT tag and scales flow to match. + DynamicPrintConfig config = mixed_config(true); + config.set_deserialize_strict({{"layer_height", "0.2"}, {"initial_layer_print_height", "0.2"}}); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + print.process(); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + INFO("gcode bytes=" << gc.size()); + CHECK(gc.find(";HEIGHT:0.12") != std::string::npos); + CHECK(gc.find(";HEIGHT:0.08") != std::string::npos); +} + +TEST_CASE("Whole-layer mixing emits only the nominal layer height", "[MixedFilament]") +{ + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"layer_height", "0.2"}, {"initial_layer_print_height", "0.2"}}); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + print.process(); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + // No sub-layer split, so the 60/40 sub-heights must never appear. + CHECK(gc.find(";HEIGHT:0.12") == std::string::npos); + CHECK(gc.find(";HEIGHT:0.08") == std::string::npos); +} + +TEST_CASE("By-object prints without mixed filaments keep their used-filament set", "[MixedFilament]") +{ + // With no mixed slot the by-object bookkeeping stays plain: object 2 prints with filament 2, + // so both filaments are used and no mixed filament is reported. + DynamicPrintConfig config = multifilament_config(2, {{"print_sequence", "by object"}}); + const std::vector> overrides{ {}, { {"extruder", "2"} } }; + + Print print; + Model model; + init_print(std::vector{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.objects().size() == 2); + print.process(); + + CHECK(print.get_slice_used_filaments(false) == std::vector{0, 1}); + CHECK(print.get_slice_used_filaments(true) == std::vector{0, 1}); + CHECK(print.get_slice_used_mixed_filaments().empty()); +} + +TEST_CASE("By-layer prints record a mixed slot's components and the slot itself", "[MixedFilament]") +{ + // Control for the by-object case below: the by-layer path publishes the physical + // components (0-based 0 and 1) as used filaments and the mixed slot (config index 2) as + // a used mixed filament. By-object prints must report exactly the same. + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(false)); + print.process(); + + CHECK(print.get_slice_used_filaments(false) == std::vector{0, 1}); + CHECK(print.get_slice_used_mixed_filaments() == std::vector{2}); +} + +TEST_CASE("By-object prints expand a mixed slot to its components in the slice bookkeeping", "[MixedFilament]") +{ + // Sequential prints build their filament lists from unsorted per-object orderings, which + // still carry the virtual slot (config index 2). The slice-used sets and the published + // grouping result must see the physical components 0 and 1 instead, and the slot itself + // must still be reported as a used mixed filament — exactly what the by-layer path yields. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"print_sequence", "by object"}}); + + Print print; + Model model; + init_print({cube(20), cube(20)}, print, model, config); + REQUIRE(print.objects().size() == 2); + print.process(); + + const std::vector components{0, 1}; + CHECK(print.get_slice_used_filaments(false) == components); + CHECK(print.get_slice_used_filaments(true) == components); + CHECK(print.get_slice_used_mixed_filaments() == std::vector{2}); + + auto group_result = print.get_layered_nozzle_group_result(); + REQUIRE(group_result != nullptr); + CHECK(group_result->get_used_filaments() == components); +} + +TEST_CASE("By-object G-code lists a mixed slot's components in the filament header", "[MixedFilament]") +{ + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"print_sequence", "by object"}}); + + Print print; + Model model; + init_print({cube(20), cube(20)}, print, model, config); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + // The header names the filaments that must be loaded (components 1 and 2, 1-based), + // never the virtual slot 3. + CHECK(gc.find("; filament: 1,2\n") != std::string::npos); + CHECK(gc.find("; filament: 3") == std::string::npos); +} + +TEST_CASE("Print::validate rejects a mixed filament as the wipe tower filament", "[MixedFilament]") +{ + // The validate backstop refuses a mixed (virtual) slot as the wipe tower filament; the GUI hides + // the slot from that option. Two cubes on physical filaments 1 and 2 make the tower real, and the + // region roles mixed_config() points at the slot are reset so only the tower uses it. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({ + {"enable_prime_tower", "1"}, + {"wipe_tower_x", "50"}, // inside the 200x200 test bed + {"wipe_tower_y", "50"}, // (the default y, 220, is not) + {"layer_change_gcode", "G92 E0\n"}, // validate() relative-E reset, as in test_print.cpp's build_cubes + {"outer_wall_filament_id", "0"}, + {"inner_wall_filament_id", "0"}, + {"sparse_infill_filament_id", "0"}, + {"internal_solid_filament_id", "0"}, + {"top_surface_filament_id", "0"}, + {"bottom_surface_filament_id", "0"}, + }); + const std::vector> overrides{ { {"extruder", "1"} }, { {"extruder", "2"} } }; + + SECTION("a physical wipe tower filament validates") { + config.set_deserialize_strict({{"wipe_tower_filament", "2"}}); + Print print; + Model model; + init_print(std::vector{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.has_wipe_tower()); + const StringObjectException err = print.validate(); + INFO(err.string); + CHECK(err.string.empty()); + } + + SECTION("the mixed slot is refused") { + config.set_deserialize_strict({{"wipe_tower_filament", "3"}}); + Print print; + Model model; + init_print(std::vector{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.has_wipe_tower()); + const StringObjectException err = print.validate(); + CHECK_FALSE(err.string.empty()); + CHECK(err.opt_key == "wipe_tower_filament"); + } +} + +TEST_CASE("Print::validate warns when a gradient mixed filament is used without sublayer mixing", "[MixedFilament]") +{ + // A gradient mixed filament only renders its gradient with the process option enabled; without + // it ToolOrdering prints one whole component per layer and the gradient is dropped silently, + // so validate() warns whenever the slot actually takes part in the print. The layer-change + // reset avoids an unrelated relative-extrusion warning, as in the wipe tower test above. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({ + {"filament_mixed_gradient", "0,0,1"}, + {"layer_change_gcode", "G92 E0\n"}, + }); + + auto count_opt = [](Print &print, const char *opt_key) { + std::vector warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), + [&](const StringObjectException &w) { return w.opt_key == opt_key; }); + }; + + SECTION("gradient slot used, sublayer mixing off") { + Print print; + Model model; + init_print({cube(20)}, print, model, config); + std::vector warnings; + const StringObjectException err = print.validate(&warnings); + CHECK(err.string.empty()); + const auto it = std::find_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_mixed_color_sublayer"; + }); + REQUIRE(it != warnings.end()); + CHECK(it->is_warning); + CHECK(std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_mixed_color_sublayer"; + }) == 1); + } + + SECTION("sublayer mixing on") { + config.set_deserialize_strict({{"enable_mixed_color_sublayer", "1"}}); + Print print; + Model model; + init_print({cube(20)}, print, model, config); + CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); + } + + SECTION("gradient flag off") { + config.set_deserialize_strict({{"filament_mixed_gradient", "0,0,0"}}); + Print print; + Model model; + init_print({cube(20)}, print, model, config); + CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); + } + + SECTION("mixed slot not used") { + config.set_deserialize_strict({ + {"outer_wall_filament_id", "0"}, + {"inner_wall_filament_id", "0"}, + {"sparse_infill_filament_id", "0"}, + {"internal_solid_filament_id", "0"}, + {"top_surface_filament_id", "0"}, + {"bottom_surface_filament_id", "0"}, + }); + Print print; + Model model; + const std::vector> overrides{{{ "extruder", "1" }}}; + init_print(std::vector{cube(20)}, print, model, config, &overrides); + CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); + } +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 28c39c2d6a..ef42a5e897 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(${_TEST_NAME}_tests test_vendor_cache.cpp test_elephant_foot_compensation.cpp test_fill_corner_smoothing.cpp + test_filament_mixer.cpp test_fill_plane_path.cpp test_geometry.cpp test_multimaterial_segmentation.cpp @@ -29,6 +30,7 @@ add_executable(${_TEST_NAME}_tests test_mutable_priority_queue.cpp test_nozzle_volume_type.cpp test_stl.cpp + test_triangle_selector.cpp test_meshboolean.cpp test_marchingsquares.cpp test_model.cpp diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 621b35a8c7..a459bfa62d 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -501,6 +501,96 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { } } +// A mixed-color filament occupies an ordinary filament slot, and painting with it stores an +// ordinary extruder state: a project saved by BambuStudio encodes filament 5 of a 5-slot setup +// as paint state 5, with the mix described by the parallel filament_mixed_* project arrays. +SCENARIO("Mixed-color filament setup and painting round-trip through a .3mf", "[3mf][MixedFilament]") { + GIVEN("a painted model whose project config describes a mixed filament in the last slot") { + Model model; + std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; + REQUIRE(load_stl(src_file.c_str(), &model)); + model.add_default_instances(); + + // Both the exporter and the importer stage Metadata/project_settings.config through the + // model's backup path; point them at writable temp dirs. + ScopedTemporaryDir backup_dir("orca_mixed_src"); + model.set_backup_path(backup_dir.string()); + + ModelVolume* mv = model.objects.front()->volumes.front(); + { + TriangleSelector selector(mv->mesh()); + selector.set_facet(0, EnforcerBlockerType::Extruder5); // the mixed slot + selector.set_facet(1, EnforcerBlockerType::Extruder2); + REQUIRE(mv->mmu_segmentation_facets.set(selector)); + } + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("filament_colour", new ConfigOptionStrings( + { "#00AE42", "#FFFF00", "#FF0000", "#0000FF", "#FF6A26" })); + config.set_key_value("filament_is_mixed", new ConfigOptionBools( + { false, false, false, false, true })); + config.set_key_value("filament_mixed_components", new ConfigOptionStrings( + { "", "", "", "", "3,2" })); + config.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings( + { "", "", "", "", "0.4200,0.5800" })); + + WHEN("stored to and reloaded from a .3mf") { + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); + + PlateData* plate = new PlateData(); + plate->plate_index = 0; + + StoreParams store_params; + store_params.path = test_file.c_str(); + store_params.model = &model; + store_params.config = &config; + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; + store_params.plate_data_list.push_back(plate); + REQUIRE(store_bbs_3mf(store_params)); + + Model dst_model; + ScopedTemporaryDir dst_backup_dir("orca_mixed_dst"); + dst_model.set_backup_path(dst_backup_dir.string()); + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector project_presets; + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + REQUIRE(load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, + &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); + + THEN("the mixed-filament project keys survive") { + auto* is_mixed = dst_config.option("filament_is_mixed"); + REQUIRE(is_mixed != nullptr); + REQUIRE(is_mixed->values == std::vector({ 0, 0, 0, 0, 1 })); + + auto* components = dst_config.option("filament_mixed_components"); + REQUIRE(components != nullptr); + REQUIRE(components->values.size() == 5); + REQUIRE(components->values[4] == "3,2"); + + auto* ratios = dst_config.option("filament_mixed_sublayer_ratios"); + REQUIRE(ratios != nullptr); + REQUIRE(ratios->values.size() == 5); + REQUIRE(ratios->values[4] == "0.4200,0.5800"); + } + + THEN("the painted facets survive, including the one painted with the mixed slot") { + REQUIRE(dst_model.objects.size() == 1); + ModelVolume* dst_mv = dst_model.objects.front()->volumes.front(); + REQUIRE_FALSE(dst_mv->mmu_segmentation_facets.empty()); + REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder2)); + REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder5)); + } + + release_PlateData_list(dst_plates); + delete plate; // store_bbs_3mf does not take ownership of the source plate + } + } +} // Locks the serialization contract of the "Publish" metadata: the orca_published flag and the // orca_published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf -> // load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior is exercised diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp new file mode 100644 index 0000000000..ade0c910dc --- /dev/null +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -0,0 +1,205 @@ +#include + +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/PrintConfig.hpp" + +using namespace Slic3r; + +TEST_CASE("parse_mixed_components reads 1-based component ids", "[FilamentMixer]") +{ + REQUIRE(parse_mixed_components("1,3") == std::vector{1, 3}); + REQUIRE(parse_mixed_components("2, 4 ,5") == std::vector{2, 4, 5}); + + SECTION("Malformed input yields no components") { + REQUIRE(parse_mixed_components("").empty()); + REQUIRE(parse_mixed_components("abc").empty()); + } +} + +TEST_CASE("parse_mixed_ratios normalizes to sum 1.0", "[FilamentMixer]") +{ + auto r = parse_mixed_ratios("0.7,0.3", 2); + REQUIRE(r.size() == 2); + REQUIRE_THAT(r[0], Catch::Matchers::WithinAbs(0.7, 1e-9)); + REQUIRE_THAT(r[1], Catch::Matchers::WithinAbs(0.3, 1e-9)); + + SECTION("Unnormalized input is rescaled") { + auto v = parse_mixed_ratios("2,2", 2); + REQUIRE_THAT(v[0], Catch::Matchers::WithinAbs(0.5, 1e-9)); + REQUIRE_THAT(v[1], Catch::Matchers::WithinAbs(0.5, 1e-9)); + } + + SECTION("Empty or mismatched input falls back to equal shares") { + auto v = parse_mixed_ratios("", 3); + REQUIRE(v.size() == 3); + for (double x : v) + REQUIRE_THAT(x, Catch::Matchers::WithinAbs(1.0 / 3.0, 1e-9)); + } +} + +TEST_CASE("has_any_mixed_filament detects mixed slots", "[FilamentMixer]") +{ + REQUIRE_FALSE(has_any_mixed_filament({})); + REQUIRE_FALSE(has_any_mixed_filament({0, 0, 0})); + REQUIRE(has_any_mixed_filament({0, 1, 0})); +} + +TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", "[FilamentMixer]") +{ + // Slot 2 (0-based) is a mix of physical filaments 1 and 2 (1-based) => 0 and 1 (0-based). + const std::vector is_mixed = {0, 0, 1}; + const std::vector comp_strs = {"", "", "1,2"}; + + REQUIRE(expand_mixed_filaments({2}, is_mixed, comp_strs) == std::vector{0, 1}); + + SECTION("Non-mixed entries pass through, result is sorted and deduplicated") { + REQUIRE(expand_mixed_filaments({2, 0}, is_mixed, comp_strs) == std::vector{0, 1}); + } +} + +TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]") +{ + const std::vector is_mixed = {0, 0, 1}; + + SECTION("All components resolve") { + REQUIRE(check_mixed_filament_integrity(is_mixed, {"", "", "1,2"}, 2).empty()); + } + + SECTION("A component past the physical filament count is broken") { + auto broken = check_mixed_filament_integrity(is_mixed, {"", "", "1,9"}, 2); + REQUIRE(broken == std::vector{2}); + } +} + +TEST_CASE("remap_mixed_components_on_delete rewrites ids around the deleted slot", "[FilamentMixer]") +{ + const std::vector is_mixed = {0, 0, 0, 1}; + std::vector comps = {"", "", "", "1,3"}; + + SECTION("Deleting a filament below the references shifts them down") { + remap_mixed_components_on_delete(is_mixed, comps, 2); + REQUIRE(comps[3] == "1,2"); + } + + SECTION("Deleting a referenced filament zeroes that component") { + remap_mixed_components_on_delete(is_mixed, comps, 1); + // 1 -> 0 (deleted sentinel), 3 -> 2 + REQUIRE(comps[3] == "0,2"); + } +} + +TEST_CASE("check_mixed_filament_type_consistency flags mismatched component types", "[FilamentMixer]") +{ + const std::vector is_mixed = {0, 0, 1}; + const std::vector comp_strs = {"", "", "1,2"}; + + REQUIRE(check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PLA"}).empty()); + + auto bad = check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PETG"}); + REQUIRE(bad == std::vector{2}); +} + +TEST_CASE("a support-flagged component reads as its own filament type for the consistency check", "[FilamentMixer]") +{ + // The sidebar derives each component's type through DynamicPrintConfig::get_filament_type, + // which folds filament_is_support into the type, so toggling that flag alone flips the + // verdict and the mixed filament list has to be refreshed on filament_is_support too. + DynamicPrintConfig plain_pla; + plain_pla.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + plain_pla.set_key_value("filament_is_support", new ConfigOptionBools({false})); + std::string displayed; + REQUIRE(plain_pla.get_filament_type(displayed) == "PLA"); + + DynamicPrintConfig support_pla; + support_pla.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + support_pla.set_key_value("filament_is_support", new ConfigOptionBools({true})); + REQUIRE(support_pla.get_filament_type(displayed) == "PLA-S"); + REQUIRE(displayed == "Sup.PLA"); + + const std::vector is_mixed = {0, 0, 1}; + const std::vector comp_strs = {"", "", "1,2"}; + REQUIRE(check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PLA-S"}) == std::vector{2}); +} + +TEST_CASE("gradient curves round-trip and sample monotonically", "[FilamentMixer]") +{ + SECTION("Empty input yields an empty curve") { + REQUIRE(parse_gradient_curve("").empty()); + REQUIRE(serialize_gradient_curve(GradientCurve{}).empty()); + } + + SECTION("Legacy 2-field anchors survive a parse/serialize round trip") { + GradientCurve c = parse_gradient_curve("0,0.15|0.5,0.5|1,0.85"); + REQUIRE(c.points.size() == 3); + + // Anchors with no tangent override serialize back to the 2-field legacy form + // (canonical fixed-precision, so compare by re-parsing rather than by string). + const std::string round_tripped = serialize_gradient_curve(c); + REQUIRE(round_tripped.find(",nan") == std::string::npos); + + GradientCurve c2 = parse_gradient_curve(round_tripped); + REQUIRE(c2.points.size() == c.points.size()); + for (size_t i = 0; i < c.points.size(); ++i) { + REQUIRE_THAT(c2.points[i].x, Catch::Matchers::WithinAbs(c.points[i].x, 1e-4)); + REQUIRE_THAT(c2.points[i].y, Catch::Matchers::WithinAbs(c.points[i].y, 1e-4)); + } + } + + SECTION("Sampling is clamped at the ends and monotone in between") { + GradientCurve c = parse_gradient_curve("0,0.15|0.5,0.5|1,0.85"); + REQUIRE_THAT(sample_gradient_curve(c, 0.0), Catch::Matchers::WithinAbs(0.15, 1e-9)); + REQUIRE_THAT(sample_gradient_curve(c, 1.0), Catch::Matchers::WithinAbs(0.85, 1e-9)); + // Outside the control point range the end values are held. + REQUIRE_THAT(sample_gradient_curve(c, -1.0), Catch::Matchers::WithinAbs(0.15, 1e-9)); + REQUIRE_THAT(sample_gradient_curve(c, 2.0), Catch::Matchers::WithinAbs(0.85, 1e-9)); + + double prev = sample_gradient_curve(c, 0.0); + for (int i = 1; i <= 20; ++i) { + double v = sample_gradient_curve(c, i / 20.0); + REQUIRE(v >= prev - 1e-9); + prev = v; + } + } + + SECTION("A curve with fewer than two points falls back to 0.5") { + GradientCurve c = parse_gradient_curve("0.5,0.7"); + REQUIRE_THAT(sample_gradient_curve(c, 0.3), Catch::Matchers::WithinAbs(0.5, 1e-9)); + } +} + +TEST_CASE("blend_color mixes two hex colors", "[FilamentMixer]") +{ + // ratio 0 keeps the first color, ratio 1 the second. + REQUIRE(blend_color("#FF0000", "#0000FF", 0.0f) == "#FF0000"); + REQUIRE(blend_color("#FF0000", "#0000FF", 1.0f) == "#0000FF"); + + SECTION("Blue and yellow make green, not grey (pigment mixing)") { + // The polynomial model approximates subtractive pigment behaviour. + std::string mixed = blend_color("#0021D0", "#FCD300", 0.5f); + REQUIRE(mixed.size() == 7); + REQUIRE(mixed[0] == '#'); + auto comp = [&](int i) { return std::stoi(mixed.substr(1 + 2 * i, 2), nullptr, 16); }; + // Green channel should dominate red and blue. + REQUIRE(comp(1) > comp(0)); + REQUIRE(comp(1) > comp(2)); + } +} + +TEST_CASE("blend_color_multi weights components", "[FilamentMixer]") +{ + SECTION("A single component is returned unchanged") { + REQUIRE(blend_color_multi({"#FF0000"}, {1}) == "#FF0000"); + } + + SECTION("Mixing a color with itself stays close to that color") { + // The mixer is a degree-4 polynomial fit of pigment behaviour, so mixing a color with + // itself lands near it rather than exactly on it; allow a small per-channel drift. + std::string mixed = blend_color_multi({"#123456", "#123456"}, {1, 1}); + REQUIRE(mixed.size() == 7); + auto comp = [](const std::string &hex, int i) { + return std::stoi(hex.substr(1 + 2 * i, 2), nullptr, 16); + }; + for (int i = 0; i < 3; ++i) + REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8); + } +} diff --git a/tests/libslic3r/test_fill_corner_smoothing.cpp b/tests/libslic3r/test_fill_corner_smoothing.cpp index f2c25e816d..a9e752f250 100644 --- a/tests/libslic3r/test_fill_corner_smoothing.cpp +++ b/tests/libslic3r/test_fill_corner_smoothing.cpp @@ -171,3 +171,24 @@ TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", REQUIRE(retrace.front() == sharp.front()); REQUIRE(retrace.back() == sharp.back()); } + +TEST_CASE("Corner smoothing ignores vertices splitting a straight leg", "[FillCornerSmoothing][Regression]") +{ + // The triangular and grid infills emit a vertex halfway along the straight run joining two of + // their corners. Measuring the legs up to that vertex instead of up to the next corner let the + // rounding reach only half as far there as it did into the very same run elsewhere in the + // pattern, so geometrically identical corners came out rounded to different radii. + const Polyline plain{ Point::new_scale(0., 20.), Point::new_scale(10., 0.), + Point::new_scale(20., 0.), Point::new_scale(30., 20.) }; + Polyline split = plain; + split.points.insert(split.points.begin() + 2, Point::new_scale(15., 0.)); + + Polyline smooth_plain = plain; + smooth_polyline_corners(smooth_plain, 1., tolerance); + Polyline smooth_split = split; + smooth_polyline_corners(smooth_split, 1., tolerance); + + REQUIRE(smooth_split.points == smooth_plain.points); + // Both corners reach the middle of the 10mm run they share, which the extra vertex sat on. + REQUIRE(contains(smooth_plain, Point::new_scale(15., 0.))); +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 34cbcfaf4e..01abba75bf 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -599,6 +599,143 @@ TEST_CASE("A printer specific filament supersedes the generic library filament w CHECK(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_c, nullptr))); } +namespace { + +const char *kMixedKeys[] = { + "filament_is_mixed", + "filament_mixed_components", + "filament_mixed_sublayer_ratios", + "filament_mixed_gradient", + "filament_mixed_gradient_range", + "filament_mixed_gradient_curve", + "filament_mixed_gradient_per_part", +}; + +} // namespace + +// Mixed-color filament metadata lives in project_config as parallel per-filament arrays. +// set_num_filaments() is the single place that grows them alongside filament_colour; if it +// misses them, creating a mixed slot writes past the end of the short arrays. +TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament count", "[Preset][Bundle][FilamentMixer]") +{ + auto mixed_array_size = [](const DynamicPrintConfig &cfg, const std::string &key) -> size_t { + if (const auto *b = cfg.option(key)) + return b->values.size(); + if (const auto *s = cfg.option(key)) + return s->values.size(); + return size_t(-1); // key missing entirely + }; + + PresetBundle bundle; + + const unsigned int n = GENERATE(2u, 4u, 8u); + bundle.set_num_filaments(n, std::string("#FF0000")); + + REQUIRE(bundle.project_config.option("filament_colour")->values.size() == n); + for (const char *key : kMixedKeys) { + DYNAMIC_SECTION("grown: " << key) { + CHECK(mixed_array_size(bundle.project_config, key) == n); + } + } + + SECTION("shrinking keeps them in step too") { + bundle.set_num_filaments(1, std::string("#00FF00")); + REQUIRE(bundle.project_config.option("filament_colour")->values.size() == 1); + for (const char *key : kMixedKeys) + CHECK(mixed_array_size(bundle.project_config, key) == 1); + } +} + +// A mix is described by 1-based indices into the project's filament list, which Orca rebuilds +// from the selected printer's snapshot (filament_%02u / filament_colors) at startup and on every +// printer selection. Held anywhere but that same per-printer snapshot, the mixed arrays end up +// indexing a filament list they were never saved against. +TEST_CASE("Mixed-color filament metadata is snapshotted per printer, with its filament list", "[Preset][Bundle][FilamentMixer]") +{ + PresetBundle bundle; + // export_selections skips the built-in "Default Printer" placeholder entirely. + add_inmemory_preset(bundle.printers, "Test Printer"); + bundle.printers.select_preset_by_name("Test Printer", true); + bundle.set_num_filaments(2u, std::string("#FF0000")); + bundle.project_config.option("filament_is_mixed")->values = { false, true }; + bundle.project_config.option("filament_mixed_components")->values = { "", "1,2" }; + bundle.project_config.option("filament_mixed_sublayer_ratios")->values = { "", "0.5,0.5" }; + + AppConfig app_config; + bundle.export_selections(app_config); + + const std::string printer_name = bundle.printers.get_selected_preset_name(); + for (const char *key : kMixedKeys) { + DYNAMIC_SECTION("per printer, not global: " << key) { + CHECK(app_config.has_printer_setting(printer_name, key)); + CHECK_FALSE(app_config.has("presets", key)); + } + } + + SECTION("with the encoding load_selections reads back") { + CHECK(app_config.get_printer_setting(printer_name, "filament_is_mixed") == "0,1"); + CHECK(app_config.get_printer_setting(printer_name, "filament_mixed_components") == "|1,2"); + CHECK(app_config.get_printer_setting(printer_name, "filament_mixed_sublayer_ratios") == "|0.5,0.5"); + } +} + +// The gradient curve is the one mixed array whose values contain '|' themselves — it separates the +// control points — so it cannot be '|'-joined into the app config like its siblings without a +// multi-point curve being split across filament slots on the way back in. +TEST_CASE("A multi-point gradient curve survives the app-config snapshot", "[Preset][Bundle][FilamentMixer]") +{ + const std::vector curves = { "", "", "0,0|0.5,0.3|1,1" }; + + PresetBundle bundle; + add_inmemory_preset(bundle.printers, "Test Printer"); + bundle.printers.select_preset_by_name("Test Printer", true); + bundle.set_num_filaments(3u, std::string("#FF0000")); + bundle.project_config.option("filament_mixed_gradient_curve")->values = curves; + + AppConfig app_config; + bundle.export_selections(app_config); + + // Decoding the stored form returns the three slots intact, curve delimiters and all. A plain + // '|' join would decode as five slots here instead of three. + std::vector decoded; + REQUIRE(unescape_strings_cstyle( + app_config.get_printer_setting(bundle.printers.get_selected_preset_name(), "filament_mixed_gradient_curve"), decoded)); + CHECK(decoded == curves); +} + +// A multi-tool printer sizes the filament list from its nozzle count. Mixed-color slots are extra +// virtual filaments at the tail of that list with no nozzle of their own, so the count has to +// allow for them: sizing to the nozzle count alone drops the project's mixes and strips every +// painted facet above the new count. +TEST_CASE("Sizing the filament list to a multi-tool nozzle count keeps mixed slots", "[Preset][Bundle][FilamentMixer]") +{ + // The 5-slot layout of a 4-tool project carrying one mix of filaments 2 and 3. + const size_t nozzle_count = 4; + PresetBundle bundle; + bundle.set_num_filaments(5u, std::string("#FF0000")); + bundle.project_config.option("filament_is_mixed")->values = + { false, false, false, false, true }; + bundle.project_config.option("filament_mixed_components")->values = + { "", "", "", "", "2,3" }; + + REQUIRE(bundle.num_mixed_filaments() == 1); + + SECTION("nozzle count plus the mixed slots preserves the mix") { + bundle.set_num_filaments(nozzle_count + bundle.num_mixed_filaments(), std::string("#00FF00")); + + CHECK(bundle.filament_presets.size() == 5); + CHECK(bundle.num_mixed_filaments() == 1); + CHECK(bundle.is_mixed_filament(4)); + CHECK(bundle.project_config.option("filament_mixed_components")->values[4] == "2,3"); + } + + SECTION("the nozzle count alone is what truncated it away") { + bundle.set_num_filaments(nozzle_count, std::string("#00FF00")); + + CHECK(bundle.filament_presets.size() == nozzle_count); + CHECK(bundle.num_mixed_filaments() == 0); + } +} // A "published" 3MF keeps the user's currently-selected presets and overlays only the // author-selected process keys onto the edited preset (mirrors the GUI load path: normalize // before load_config_model, then the overlay in load_config_file_config). diff --git a/tests/libslic3r/test_triangle_selector.cpp b/tests/libslic3r/test_triangle_selector.cpp new file mode 100644 index 0000000000..fd2ab9efa8 --- /dev/null +++ b/tests/libslic3r/test_triangle_selector.cpp @@ -0,0 +1,125 @@ +#include + +#include "libslic3r/TriangleSelector.hpp" +#include "libslic3r/TriangleMesh.hpp" + +using namespace Slic3r; + +// A sphere gives well over ExtruderMax original facets, so every extruder state can be assigned +// to a facet of its own without any splitting getting in the way. +static TriangleMesh test_mesh() { return make_sphere(5., 2 * PI / 24); } + +// Read the nibble_idx-th 4-bit group of a serialized bitstream, least significant bit first. +static int nibble_at(const std::vector &bitstream, size_t nibble_idx) +{ + int n = 0; + for (size_t bit = 0; bit < 4; ++bit) + n |= int(bitstream[nibble_idx * 4 + bit]) << bit; + return n; +} + +TEST_CASE("Every extruder state survives a serialize/deserialize round trip", "[TriangleSelector]") +{ + const TriangleMesh mesh = test_mesh(); + const int max_state = int(EnforcerBlockerType::ExtruderMax); + REQUIRE(int(mesh.its.indices.size()) >= max_state); + + TriangleSelector selector(mesh); + for (int state = 1; state <= max_state; ++state) + selector.set_facet(state - 1, EnforcerBlockerType(state)); + + TriangleSelector restored(mesh); + restored.deserialize(selector.serialize()); + + for (int state = 1; state <= max_state; ++state) { + INFO("Extruder " << state); + REQUIRE(restored.has_facets(EnforcerBlockerType(state))); + REQUIRE(restored.num_facets(EnforcerBlockerType(state)) == 1); + } +} + +TEST_CASE("Serialized data reports the extruder states it uses", "[TriangleSelector]") +{ + const TriangleMesh mesh = test_mesh(); + TriangleSelector selector(mesh); + selector.set_facet(0, EnforcerBlockerType::Extruder16); + selector.set_facet(1, EnforcerBlockerType::Extruder32); + + const TriangleSelector::TriangleSplittingData data = selector.serialize(); + + REQUIRE(data.used_states.size() == size_t(EnforcerBlockerType::ExtruderMax) + 1); + REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder16)]); + REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder32)]); + REQUIRE_FALSE(data.used_states[size_t(EnforcerBlockerType::Extruder17)]); + + SECTION("used_states recomputed from the bitstream agrees") { + TriangleSelector::TriangleSplittingData recomputed = data; + recomputed.reset_used_states(); + recomputed.update_used_states(0); + REQUIRE(recomputed.used_states == data.used_states); + } + + SECTION("has_facets on the raw data agrees") { + REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder32)); + REQUIRE_FALSE(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder17)); + } +} + +// States 3..17 must keep the pre-existing encoding ("11" prefix plus one nibble of state-3) so +// projects written by older builds stay readable and newly written ones stay readable by them. +TEST_CASE("Extruder states up to 17 keep the single-nibble encoding", "[TriangleSelector]") +{ + const int state = GENERATE(3, 8, 16, 17); + + TriangleSelector selector(test_mesh()); + selector.set_facet(0, EnforcerBlockerType(state)); + const std::vector bitstream = selector.serialize().bitstream; + + INFO("Extruder " << state); + // Two nibbles: the "11"-prefixed leaf code, then the state itself. + REQUIRE(bitstream.size() == 8); + REQUIRE(nibble_at(bitstream, 0) == 0b1100); + REQUIRE(nibble_at(bitstream, 1) == state - 3); +} + +// States 18 and above set the state nibble to 0b1111 and carry (state-18) in one more nibble. +TEST_CASE("Extruder states above 17 are encoded in a second nibble", "[TriangleSelector]") +{ + const int state = GENERATE(18, 25, 32); + + TriangleSelector selector(test_mesh()); + selector.set_facet(0, EnforcerBlockerType(state)); + const std::vector bitstream = selector.serialize().bitstream; + + INFO("Extruder " << state); + REQUIRE(bitstream.size() == 12); + REQUIRE(nibble_at(bitstream, 0) == 0b1100); + REQUIRE(nibble_at(bitstream, 1) == 0b1111); + REQUIRE(nibble_at(bitstream, 2) == state - 18); +} + +// Model.cpp writes these hex strings into the 3MF for colored mesh imports; the selector must +// decode exactly the states CONST_FILAMENTS assigns to them. +TEST_CASE("Extruder states match the CONST_FILAMENTS hex encoding", "[TriangleSelector]") +{ + struct Case { const char *hex; int state; }; + const auto c = GENERATE(values({ + {"8", 2}, {"0C", 3}, {"DC", 16}, {"EC", 17}, {"0FC", 18}, {"EFC", 32}, + })); + + // get_triangle_as_string emits the nibbles most significant first, so read the hex backwards. + const std::string hex = c.hex; + std::vector bitstream; + for (auto it = hex.rbegin(); it != hex.rend(); ++it) { + const int nibble = *it >= 'A' ? (*it - 'A' + 10) : (*it - '0'); + for (int bit = 0; bit < 4; ++bit) + bitstream.push_back((nibble >> bit) & 1); + } + + TriangleSelector::TriangleSplittingData data; + data.triangles_to_split.emplace_back(0, 0); + data.bitstream = bitstream; + + INFO("Hex " << c.hex << " -> extruder " << c.state); + REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType(c.state))); +} diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index c1424064b2..ebbd62b820 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -2,6 +2,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp test_dev_mapping.cpp + test_filament_bitmap_utils.cpp test_network_versions.cpp test_action_source.cpp test_plugin_host_api.cpp diff --git a/tests/slic3rutils/test_filament_bitmap_utils.cpp b/tests/slic3rutils/test_filament_bitmap_utils.cpp new file mode 100644 index 0000000000..997a119521 --- /dev/null +++ b/tests/slic3rutils/test_filament_bitmap_utils.cpp @@ -0,0 +1,256 @@ +// recompute_mixed_slot_colors lives in libslic3r_gui; this is the only suite that links it. +// Same Windows include prologue as test_dev_mapping.cpp (wx pulls in ; keep +// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers). +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#include + +#include + +#include +#include + +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/FilamentBitmapUtils.hpp" + +using namespace Slic3r; +using Slic3r::GUI::recompute_mixed_slot_colors; + +namespace { + +// Two physical slots (1 = red, 2 = blue) and mixed slot 3 built from them. +DynamicPrintConfig mixed_config(const std::string& components = "1,2", const std::string& ratios = "0.5,0.5") +{ + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", components})); + cfg.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings({"", "", ratios})); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false})); + cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#0000FF", "#000000"})); + return cfg; +} + +wxColour expected_blend(const std::vector& hex, const std::vector& weights) +{ + return wxColour(wxString(blend_color_multi(hex, weights))); +} + +// Compare channels one at a time so a failure names the channel. +void require_same_rgb(const wxColour& actual, const wxColour& expected) +{ + REQUIRE(int(actual.Red()) == int(expected.Red())); + REQUIRE(int(actual.Green()) == int(expected.Green())); + REQUIRE(int(actual.Blue()) == int(expected.Blue())); +} + +} // namespace + +TEST_CASE("recompute_mixed_slot_colors blends a mixed slot from its components' colours", "[FilamentBitmapUtils]") +{ + std::vector colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + recompute_mixed_slot_colors(colors, mixed_config()); + + REQUIRE(colors.size() == 3); + require_same_rgb(colors[2], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); + REQUIRE(int(colors[2].Alpha()) == 255); + // Physical slots are left alone. + require_same_rgb(colors[0], wxColour(255, 0, 0)); + require_same_rgb(colors[1], wxColour(0, 0, 255)); +} + +TEST_CASE("recompute_mixed_slot_colors leaves the colours alone without mixed slots", "[FilamentBitmapUtils]") +{ + std::vector colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + + SECTION("no mixed keys at all") { + recompute_mixed_slot_colors(colors, DynamicPrintConfig{}); + } + SECTION("mixed flags present but all false") { + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", ""})); + recompute_mixed_slot_colors(colors, cfg); + } + REQUIRE(colors.size() == 2); + require_same_rgb(colors[0], wxColour(255, 0, 0)); + require_same_rgb(colors[1], wxColour(0, 0, 255)); +} + +TEST_CASE("recompute_mixed_slot_colors falls back to grey for a broken component reference", "[FilamentBitmapUtils]") +{ + const wxColour grey(128, 128, 128, 255); + std::vector colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + + SECTION("dangling component id") { + recompute_mixed_slot_colors(colors, mixed_config("1,9")); + } + SECTION("empty component list") { + recompute_mixed_slot_colors(colors, mixed_config("")); + } + REQUIRE(colors.size() == 3); + require_same_rgb(colors[2], grey); +} + +TEST_CASE("recompute_mixed_slot_colors uses the project colour when a slot colour is unset", "[FilamentBitmapUtils]") +{ + // Slot 2 carries no colour in the vector; filament_colour[1] = "#0000FF" is used instead. + std::vector colors{wxColour(255, 0, 0), wxColour()}; + recompute_mixed_slot_colors(colors, mixed_config()); + require_same_rgb(colors[2], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); +} + +TEST_CASE("recompute_mixed_slot_colors blends a gradient slot from its end points only", "[FilamentBitmapUtils]") +{ + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, false, true})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "", "1,2,3"})); + cfg.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings({"", "", "", "0.2,0.3,0.5"})); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false, true})); + cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00", "#0000FF", "#000000"})); + + std::vector colors{wxColour(255, 0, 0), wxColour(0, 255, 0), wxColour(0, 0, 255)}; + recompute_mixed_slot_colors(colors, cfg); + + REQUIRE(colors.size() == 4); + require_same_rgb(colors[3], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); +} + +TEST_CASE("recompute_mixed_slot_colors honours the configured ratios and is idempotent", "[FilamentBitmapUtils]") +{ + std::vector colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + const DynamicPrintConfig cfg = mixed_config("1,2", "0.7,0.3"); + recompute_mixed_slot_colors(colors, cfg); + const wxColour first = colors[2]; + // The configured 70/30 ratio must reach the blend (it is not the equal-share default). + require_same_rgb(first, expected_blend({"#FF0000", "#0000FF"}, {7000, 3000})); + REQUIRE(first != expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); + recompute_mixed_slot_colors(colors, cfg); + require_same_rgb(colors[2], first); +} + +// --- mixed_gradient_ramp / sample_gradient_ramp ----------------------------------------- +// +// The ramp is what every mixed filament swatch is drawn from, so these pin the three things +// a plain fade between two endpoint colours cannot express: the reserved ratio band, the +// component order, and the custom curve. + +namespace { + +// Slot 3 (index 2) is a gradient mix of physical slots 1 (red) and 2 (blue). +DynamicPrintConfig gradient_config(const std::string& components = "1,2", + const std::string& range = "0.9,0.1", + const std::string& curve = "") +{ + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", components})); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, true})); + cfg.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({"", "", range})); + cfg.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({"", "", curve})); + cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#0000FF", "#000000"})); + return cfg; +} + +} // namespace + +TEST_CASE("mixed_gradient_ramp runs bottom to top and never reaches a pure component", "[FilamentBitmapUtils]") +{ + // range "0.9,0.1": component 1 (red) is the majority at the bottom and the minority at the top. + const auto ramp = Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 2, 16); + REQUIRE(ramp.size() == 16); + + // Neither end is the pure component colour - the slicer clamps the blend to + // [kGradientMinRatio, kGradientMaxRatio], which a fade between the pure colours would ignore. + REQUIRE(ramp.front() != wxColour(255, 0, 0)); + REQUIRE(ramp.back() != wxColour(0, 0, 255)); + + // Red falls and blue rises monotonically from bottom to top. + for (size_t i = 1; i < ramp.size(); ++i) { + REQUIRE(int(ramp[i].Red()) <= int(ramp[i - 1].Red())); + REQUIRE(int(ramp[i].Blue()) >= int(ramp[i - 1].Blue())); + } +} + +TEST_CASE("mixed_gradient_ramp follows the range's direction rather than the component order", "[FilamentBitmapUtils]") +{ + const auto rising = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.1,0.9"), 2, 16); + const auto falling = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.9,0.1"), 2, 16); + REQUIRE(rising.size() == 16); + REQUIRE(falling.size() == 16); + + // "0.1,0.9" starts blue-heavy at the bottom; "0.9,0.1" starts red-heavy. Reversing the + // range must reverse the ramp, which endpoint colours ordered by HSV cannot express. + REQUIRE(int(rising.front().Blue()) > int(rising.front().Red())); + REQUIRE(int(falling.front().Red()) > int(falling.front().Blue())); + require_same_rgb(rising.front(), falling.back()); +} + +TEST_CASE("mixed_gradient_ramp bends with a custom curve", "[FilamentBitmapUtils]") +{ + // Component 1 holds near its maximum for the first half, then drops - a shape a straight + // fade between two endpoints cannot draw. + const auto curved = Slic3r::GUI::mixed_gradient_ramp( + gradient_config("1,2", "0.9,0.1", "0,0.9|0.5,0.85|1,0.1"), 2, 16); + const auto linear = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.9,0.1"), 2, 16); + REQUIRE(curved.size() == 16); + + // The curve holds component 1 high through the lower half, so every band up to mid height + // is at least as red as the straight fade and mid height is strictly redder. + for (size_t i = 0; i <= curved.size() / 2; ++i) + REQUIRE(int(curved[i].Red()) >= int(linear[i].Red())); + REQUIRE(int(curved[curved.size() / 2].Red()) > int(linear[linear.size() / 2].Red())); + // It still ends blue-dominant, like the straight fade. + REQUIRE(int(curved.back().Blue()) > int(curved.back().Red())); +} + +TEST_CASE("mixed_gradient_ramp is empty for anything but a two-component gradient slot", "[FilamentBitmapUtils]") +{ + SECTION("slot is not mixed") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 0, 16).empty()); + } + SECTION("gradient is off") { + DynamicPrintConfig cfg = gradient_config(); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false})); + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(cfg, 2, 16).empty()); + } + SECTION("three components") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2,3"), 2, 16).empty()); + } + SECTION("slot out of range") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 9, 16).empty()); + } + SECTION("no mixed keys at all") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(DynamicPrintConfig{}, 0, 16).empty()); + } +} + +TEST_CASE("sample_gradient_ramp blends each step through the shared blender", "[FilamentBitmapUtils]") +{ + // A flat curve makes every step the same 30/70 mix, which must come out as the blend the + // dialog's own swatches are drawn with - not a channel lerp between the two components. + GradientCurve curve; + curve.points = {{0.0, 0.3, NAN, NAN}, {1.0, 0.3, NAN, NAN}}; + const auto ramp = Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 4); + REQUIRE(ramp.size() == 4); + + const wxColour expected = Slic3r::GUI::blend_n_colors({wxColour(255, 0, 0), wxColour(0, 0, 255)}, {0.3, 0.7}); + for (const wxColour& c : ramp) + require_same_rgb(c, expected); +} + +TEST_CASE("sample_gradient_ramp returns nothing without a usable curve or step count", "[FilamentBitmapUtils]") +{ + GradientCurve curve; + REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 8).empty()); + curve.points = {{0.0, kGradientMaxRatio, NAN, NAN}, {1.0, kGradientMinRatio, NAN, NAN}}; + REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 0).empty()); +}