Merge branch 'main' into feature/filament_id

This commit is contained in:
SoftFever
2026-08-30 20:54:33 +08:00
258 changed files with 42147 additions and 2744 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ jobs:
working-directory: ${{ github.workspace }}
run: |
if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install automake texinfo libtool
brew install automake texinfo libtool pkgconf yasm nasm
fi
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
+7
View File
@@ -385,6 +385,13 @@ jobs:
dir "C:/Program Files (x86)/Windows Kits/10/Include"
choco install nsis
- name: Install pkg-config
# FFmpeg is discovered via pkg-config (pkg_check_modules LIBAV in
# src/slic3r/CMakeLists.txt); the Windows runners don't ship it.
if: runner.os == 'Windows' && !vars.SELF_HOSTED
run: |
choco install pkgconfiglite -y
- name: Build slicer Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
+1
View File
@@ -2,6 +2,7 @@ Build
Build.bat
/build*/
CMakeLists.txt.user
CMakeUserPresets.json
**/CMakeLists.txt.autosave
deps/build*
MYMETA.json
+150 -9
View File
@@ -59,6 +59,13 @@ if (APPLE)
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif ()
# Keep MSVC's default /W3 out of CMAKE_<LANG>_FLAGS so it can be applied to our own
# targets only. Silencing a bundled target would otherwise override a warning level,
# which cl reports as D9025 for every file it compiles.
if (POLICY CMP0092)
cmake_policy(SET CMP0092 NEW)
endif ()
project(OrcaSlicer)
# Backward compatibility for old CMake versions
@@ -126,6 +133,8 @@ option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL,
option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0)
option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0)
option(SLIC3R_PCH "Use precompiled headers" 1)
option(SLIC3R_WARNINGS "Emit compiler warnings for OrcaSlicer sources" 1)
option(SLIC3R_BUNDLED_WARNINGS "Emit compiler warnings for bundled third-party sources" 0)
option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1)
option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1)
option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
@@ -289,6 +298,8 @@ if (APPLE)
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
endif ()
# Proposal for C++ unit tests and sandboxes
@@ -335,15 +346,20 @@ if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
# clang-cl can interpret SYSTEM header paths if -imsvc is used
set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "-imsvc")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall \
-Wno-old-style-cast -Wno-reserved-id-macro -Wno-c++98-compat-pedantic")
else ()
set(IS_CLANG_CL FALSE)
endif ()
if (MSVC)
if (SLIC3R_MSVC_COMPILE_PARALLEL AND NOT IS_CLANG_CL)
# CMP0092 only applies when the cache is created; an existing tree keeps its /W3,
# which a silenced bundled target would then override (D9025, once per file).
string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
# /MP only matters for the VS generators, where CMake turns it into the
# MultiProcessorCompilation property. Ninja parallelises on its own, and
# clang-cl warns "argument unused" if the flag reaches it.
if (SLIC3R_MSVC_COMPILE_PARALLEL AND CMAKE_GENERATOR MATCHES "Visual Studio")
add_compile_options(/MP)
endif ()
# /bigobj (Increase Number of Sections in .Obj file)
@@ -460,7 +476,8 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32)
if(WIN32)
find_package(PkgConfig REQUIRED)
# if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
@@ -496,7 +513,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# else()
# message("Building without Win10 Netfabb STL fixing service support")
# endif()
# endif()
endif()
if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
@@ -523,8 +540,15 @@ if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fext-numeric-literals" )
endif()
if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
if (NOT MINGW)
if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
if (IS_CLANG_CL)
# clang-cl reads -Wall as MSVC /Wall, which clang maps to -Weverything. /W4 is
# its -Wall -Wextra and, unlike /clang:-Wall, is ordered with the -Wno-* below
# instead of after them. The -Wextra-only warnings are dropped again so the set
# matches what -Wall gives the GNU/Clang builds.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4" )
add_compile_options(-Wno-unused-parameter -Wno-ignored-qualifiers -Wno-missing-field-initializers)
elseif (NOT MINGW)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" )
endif ()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder" )
@@ -1044,6 +1068,10 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll
${CMAKE_PREFIX_PATH}/bin/freetype.dll
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
${CMAKE_PREFIX_PATH}/bin/avutil-59.dll
DESTINATION ${_out_dir})
set(${output_dlls}
@@ -1079,15 +1107,110 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${_out_dir}/TKXSBase.dll
${_out_dir}/freetype.dll
${_out_dir}/avcodec-61.dll
${_out_dir}/swresample-5.dll
${_out_dir}/swscale-8.dll
${_out_dir}/avutil-59.dll
PARENT_SCOPE
)
endfunction()
function(orcaslicer_copy_sos target config postfix output_sos)
get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
get_target_property(_alt_out_dir ${target} RUNTIME_OUTPUT_DIRECTORY)
if (_alt_out_dir)
set(_out_dir "${_alt_out_dir}")
elseif (_is_multi)
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}/${config}")
else ()
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
endif ()
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
${CMAKE_PREFIX_PATH}/lib/libavutil.so
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59.8.100
${CMAKE_PREFIX_PATH}/lib/libswscale.so
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8.1.100
${CMAKE_PREFIX_PATH}/lib/libswresample.so
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5.1.100
DESTINATION ${_out_dir})
set(${output_sos}
${_out_dir}/libavcodec.so
${_out_dir}/libavcodec.so.61
${_out_dir}/libavcodec.so.61.3.100
${_out_dir}/libavutil.so
${_out_dir}/libavutil.so.59
${_out_dir}/libavutil.so.59.8.100
${_out_dir}/libswscale.so
${_out_dir}/libswscale.so.8
${_out_dir}/libswscale.so.8.1.100
${_out_dir}/libswresample.so
${_out_dir}/libswresample.so.5
${_out_dir}/libswresample.so.5.1.100
PARENT_SCOPE
)
endfunction()
# Bundled sources set their own warning flags, and a plain -Wall there means /Wall
# (= -Weverything) under clang-cl. Target options are applied after the ones a target
# set on itself, so these win. Targets are discovered rather than listed so a newly
# bundled library needs no maintenance here.
function(orcaslicer_silence_third_party_warnings _dir)
get_property(_subdirs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES)
foreach (_subdir IN LISTS _subdirs)
orcaslicer_silence_third_party_warnings("${_subdir}")
endforeach ()
get_property(_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS)
foreach (_target IN LISTS _targets)
get_target_property(_type ${_target} TYPE)
if (NOT _type STREQUAL "INTERFACE_LIBRARY" AND NOT _type STREQUAL "UTILITY")
if (MSVC AND NOT IS_CLANG_CL)
# Drop any level the target set for itself, or -w overrides it and cl
# reports D9025 once per file.
get_target_property(_opts ${_target} COMPILE_OPTIONS)
if (_opts)
string(REGEX REPLACE "/W[0-4]|/Wall" "" _opts "${_opts}")
string(REGEX REPLACE ";;+" ";" _opts "${_opts}")
set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}")
endif ()
# CMake maps a level into the VS generator's WarningLevel element, while a
# bare -w stays on the command line and trips D9025 there, once per file.
target_compile_options(${_target} PRIVATE /W0)
else ()
target_compile_options(${_target} PRIVATE -w)
endif ()
endif ()
endforeach ()
endfunction()
# libslic3r, OrcaSlicer GUI and the OrcaSlicer executable.
add_subdirectory(deps_src)
if (NOT SLIC3R_BUNDLED_WARNINGS)
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/deps_src")
endif ()
# Warning level for the targets added below: our sources, plus glad and libvgcode,
# which are vendored but live under src/. The deps_src libraries were configured just
# above. CMP0092 left MSVC without a default level, so it is set here.
if (NOT SLIC3R_WARNINGS)
add_compile_options(-w)
elseif (MSVC AND NOT IS_CLANG_CL)
# /we4715 is C4715, no return from a non-void function, matching the
# -Werror=return-type the GNU/Clang builds apply.
add_compile_options(/W3 /we4715)
endif ()
add_subdirectory(src)
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT OrcaSlicer_app_gui)
@@ -1099,6 +1222,10 @@ endif()
if(BUILD_TESTS)
add_subdirectory(tests)
if (NOT SLIC3R_BUNDLED_WARNINGS)
# Catch2 is vendored under tests/ and sets its own warning flags too.
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/tests/catch2")
endif ()
endif()
if (NOT WIN32 AND NOT APPLE)
@@ -1143,6 +1270,20 @@ else ()
endif()
endif ()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LIBRARY_FILES
${LIBDIR_BIN}/libavcodec.so.61
${LIBDIR_BIN}/libavcodec.so.61.3.100
${LIBDIR_BIN}/libavutil.so.59
${LIBDIR_BIN}/libavutil.so.59.8.100
${LIBDIR_BIN}/libswresample.so.5
${LIBDIR_BIN}/libswresample.so.5.1.100
${LIBDIR_BIN}/libswscale.so.8
${LIBDIR_BIN}/libswscale.so.8.1.100
)
install(FILES ${LIBRARY_FILES} DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
endif ()
install(FILES ${CMAKE_SOURCE_DIR}/LICENSE.txt DESTINATION ".")
configure_file(${LIBDIR}/dev-utils/platform/unix/fhs.hpp.in ${LIBDIR_BIN}/dev-utils/platform/unix/fhs.hpp)
+16 -3
View File
@@ -20,6 +20,18 @@ for %%a in (%*) do (
if "%%a"=="-x" set USE_NINJA=1
)
@REM Check for clang-cl option (-l). Combined with -x it also builds the deps with
@REM clang-cl; on the Visual Studio generator it applies to the slicer only, because
@REM the dependency sub-builds have no toolset to inherit and stay on MSVC.
set CLANG_ARG=
set TOOLSET_ARG=
for %%a in (%*) do (
if "%%a"=="-l" (
set CLANG_ARG=-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl
set TOOLSET_ARG=-T ClangCL
)
)
@REM Check for unit-tests option ("tests")
set BUILD_TESTS=OFF
for %%a in (%*) do (
@@ -127,12 +139,13 @@ if "%1"=="slicer" (
GOTO :slicer
)
echo "building deps.."
if defined CLANG_ARG if "%USE_NINJA%"=="0" echo Note: -l needs -x for the dependencies; building them with MSVC.
echo on
REM Set minimum CMake policy to avoid <3.5 errors
set CMAKE_POLICY_VERSION_MINIMUM=3.5
if "%USE_NINJA%"=="1" (
cmake ../ -G %CMAKE_GENERATOR% -DCMAKE_BUILD_TYPE=%build_type%
cmake ../ -G %CMAKE_GENERATOR% %CLANG_ARG% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps
) else (
cmake ../ -G %CMAKE_GENERATOR% -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
@@ -151,10 +164,10 @@ cd %build_dir%
echo on
set CMAKE_POLICY_VERSION_MINIMUM=3.5
if "%USE_NINJA%"=="1" (
cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake .. -G %CMAKE_GENERATOR% %CLANG_ARG% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target all
) else (
cmake .. -G %CMAKE_GENERATOR% -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake .. -G %CMAKE_GENERATOR% -A %arch% %TOOLSET_ARG% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
)
@echo off
+43
View File
@@ -0,0 +1,43 @@
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 <stdio.h> 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
# Assimp's ccache support sets the global RULE_LAUNCH_COMPILE, which breaks
# the Ninja RC rule. The superbuild forwards CMAKE_<LANG>_COMPILER_LAUNCHER.
-DASSIMP_BUILD_USE_CCACHE=OFF
-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 ()
+9 -1
View File
@@ -24,6 +24,13 @@ if (MSVC AND DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
# takes volatile long*; cl compiles that with a warning, clang errors out.
set(_boost_c_flags_line "")
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types")
endif ()
orcaslicer_add_cmake_project(Boost
${_options}
URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz"
@@ -38,6 +45,7 @@ orcaslicer_add_cmake_project(Boost
"${_context_abi_line}"
"${_context_arch_line}"
"${_context_impl_line}"
"${_boost_c_flags_line}"
)
set(DEP_Boost_DEPENDS ZLIB)
set(DEP_Boost_DEPENDS ZLIB)
+16 -3
View File
@@ -157,8 +157,16 @@ endif ()
function(orcaslicer_add_cmake_project projectname)
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
# generator. A non-Visual-Studio superbuild passes its own generator down, and with
# it the CMAKE_C_COMPILER / CMAKE_CXX_COMPILER forwarded below.
set(_dep_msvc_gen FALSE)
if (MSVC AND CMAKE_GENERATOR MATCHES "Visual Studio")
set(_dep_msvc_gen TRUE)
endif ()
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
if (_is_multi OR MSVC)
if (_is_multi OR _dep_msvc_gen)
if (P_ARGS_FORWARD_CONFIG)
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
elseif (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG)
@@ -174,7 +182,7 @@ function(orcaslicer_add_cmake_project projectname)
set(_target_config "Release")
endif()
if (MSVC)
if (_dep_msvc_gen)
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
else()
set(_gen "")
@@ -182,7 +190,7 @@ function(orcaslicer_add_cmake_project projectname)
if ($ENV{CMAKE_BUILD_PARALLEL_LEVEL})
set(_build_j "") # assume environment will control --build parallel setting
elseif(MSVC)
elseif(_dep_msvc_gen)
set(_build_j "/m")
else()
set(_build_j "-j${NPROC}")
@@ -367,6 +375,9 @@ include(libnoise/libnoise.cmake)
include(Draco/Draco.cmake)
include(FFMPEG/FFMPEG.cmake)
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 +459,8 @@ set(_dep_list
dep_libnoise
dep_python3
dep_wxInspector
dep_FFMPEG
dep_Assimp
)
if (MSVC)
+14
View File
@@ -56,6 +56,18 @@ else()
set(_curl_static ON)
endif()
# curl 7.75's configure probes and code rely on C laxness cl allows but clang
# errors on (implicit function declarations, int* vs u_long* in ioctlsocket),
# which flips probe results and misconfigures nonblock.c into the AmigaOS
# IoctlSocket branch. Relax both diagnostics so the probes behave like cl, and
# pin the camel-case probes off since they only "pass" by implicit declaration.
set(_curl_c_flags_line "")
set(_curl_probe_overrides "")
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_curl_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-implicit-function-declaration -Wno-incompatible-pointer-types")
set(_curl_probe_overrides -DHAVE_IOCTLSOCKET_CAMEL=0 -DHAVE_IOCTLSOCKET_CAMEL_FIONBIO=0)
endif ()
orcaslicer_add_cmake_project(CURL
# GIT_REPOSITORY https://github.com/curl/curl.git
# GIT_TAG curl-7_75_0
@@ -69,6 +81,8 @@ orcaslicer_add_cmake_project(CURL
-DBUILD_CURL_EXE:BOOL=OFF
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DCURL_STATICLIB=${_curl_static}
"${_curl_c_flags_line}"
${_curl_probe_overrides}
${_curl_platform_flags}
)
+15
View File
@@ -7,5 +7,20 @@ orcaslicer_add_cmake_project(Eigen
URL https://gitlab.com/libeigen/eigen/-/archive/5.0.1/eigen-5.0.1.zip
URL_HASH SHA256=0dbb1f9e3aaad66f352c03227d8c983f6f0b49e0b07e71a7300f4abcc01aee12
CMAKE_ARGS "${_eigen_extra_flags}"
# Only the headers are consumed here. Everything below builds nothing we
# use, and all three enable_language(Fortran): test/CMakeLists.txt:9,
# lapack/CMakeLists.txt:6 and blas/testing/CMakeLists.txt:2. They default
# to ON because the dependency configures as its own top-level project.
#
# Whether that probe is harmless depends on what CMake finds. The Visual
# Studio generator supports no Fortran, so it finds nothing; clang-cl sits
# next to the LLVM toolset's flang, which works. MSVC with Ninja finds
# Strawberry Perl's MinGW gfortran instead, which the deps build already
# requires for OpenSSL, and hands it the MSVC-style /machine:x64 that
# MinGW's ld reads as a missing input file. The configure dies there and
# takes the rest of the superbuild with it.
-DEIGEN_BUILD_TESTING=OFF
-DEIGEN_BUILD_BLAS=OFF
-DEIGEN_BUILD_LAPACK=OFF
DEPENDS dep_Boost dep_GMP dep_MPFR
)
+87
View File
@@ -0,0 +1,87 @@
set(_conf_cmd ./configure)
if (MSVC)
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip")
set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054")
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip")
set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c")
ExternalProject_Add(dep_FFMPEG
URL ${PREBUILD_URL_${DEPS_ARCH}}
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/bin" "${DESTDIR}/bin"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/lib" "${DESTDIR}/lib"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/include" "${DESTDIR}/include"
)
else ()
if (APPLE)
set(_minos_cmd
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
"--extra-ldflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
)
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
# Disable the VideoToolbox/AudioToolbox HW-accel paths: the player decodes
# in software (swscale), and the auto-detected HW objects would drag in
# system frameworks that the static libs would then depend on.
set(_link_cmd --enable-static --disable-shared --disable-videotoolbox --disable-audiotoolbox)
if (IS_CROSS_COMPILE)
set(_cross_cmd --enable-cross-compile)
set(_pic_cmd --enable-pic)
if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64")
set(_arch_cmd --arch=arm64)
set(_cc_cmd "--cc=clang -arch arm64")
else()
set(_arch_cmd --arch=x86_64)
set(_cc_cmd "--cc=clang -arch x86_64")
endif()
endif()
else ()
set(_link_cmd --enable-shared)
endif ()
set(_build_j -j)
if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL})
set(_build_j "-j$ENV{CMAKE_BUILD_PARALLEL_LEVEL}")
endif()
ExternalProject_Add(dep_FFMPEG
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ${_conf_cmd}
${_cross_cmd}
${_pic_cmd}
${_arch_cmd}
${_cc_cmd}
"--prefix=${DESTDIR}"
${_link_cmd}
${_minos_cmd}
--disable-doc
--enable-small
--disable-outdevs
--disable-filters
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
--disable-protocols
--enable-protocol=file,fd,pipe,rtp,udp
--disable-muxers
--enable-muxer=rtp
--disable-encoders
--disable-decoders
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
--disable-demuxers
--enable-demuxer=h264,mp3,mov
--disable-zlib
--disable-avdevice
BUILD_IN_SOURCE ON
BUILD_COMMAND make ${_build_j}
INSTALL_COMMAND make install
)
endif()
+43
View File
@@ -1,3 +1,20 @@
diff --git a/adm/cmake/occt_defs_flags.cmake b/adm/cmake/occt_defs_flags.cmake
index 00000000..00000001 100644
--- a/adm/cmake/occt_defs_flags.cmake
+++ b/adm/cmake/occt_defs_flags.cmake
@@ -134,7 +134,11 @@
set (CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}")
endif()
# Optimize size of binaries
- set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
+ # clang-cl reports the Clang compiler ID, and OCCT builds shared on Windows,
+ # where the MSVC-style linker gets this flag as an argument it does not know.
+ if (NOT WIN32)
+ set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
+ endif()
elseif(MINGW)
add_definitions(-D_WIN32_WINNT=0x0601)
# _WIN32_WINNT=0x0601 (use Windows 7 SDK)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d98acc0f..28eb8eb4 100644
--- a/CMakeLists.txt
@@ -168,6 +185,32 @@ index d98acc0f..28eb8eb4 100644
endforeach()
if (BUILD_SAMPLES_QT)
diff --git a/adm/cmake/occt_macros.cmake b/adm/cmake/occt_macros.cmake
index 224c96b1..8c94a1c5 100644
--- a/adm/cmake/occt_macros.cmake
+++ b/adm/cmake/occt_macros.cmake
@@ -608,7 +608,7 @@ macro (OCCT_INSERT_CODE_FOR_TARGET)
install(CODE "if (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$\")
set (OCCT_INSTALL_BIN_LETTER \"\")
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ww][Ii][Tt][Hh][Dd][Ee][Bb][Ii][Nn][Ff][Oo])$\")
- set (OCCT_INSTALL_BIN_LETTER \"i\")
+ set (OCCT_INSTALL_BIN_LETTER \"\")
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Dd][Ee][Bb][Uu][Gg])$\")
set (OCCT_INSTALL_BIN_LETTER \"d\")
endif()")
diff --git a/adm/cmake/occt_toolkit.cmake b/adm/cmake/occt_toolkit.cmake
index 550e0e2f..7ac1a3b8 100644
--- a/adm/cmake/occt_toolkit.cmake
+++ b/adm/cmake/occt_toolkit.cmake
@@ -241,7 +241,7 @@
else()
set (aReleasePdbConf)
endif()
- install (FILES ${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin\${OCCT_INSTALL_BIN_LETTER}/${PROJECT_NAME}.pdb
+ install (FILES $<TARGET_PDB_FILE:${PROJECT_NAME}>
CONFIGURATIONS Debug ${aReleasePdbConf} RelWithDebInfo
DESTINATION "${INSTALL_DIR_BIN}\${OCCT_INSTALL_BIN_LETTER}")
endif()
diff --git a/src/Font/Font_FTFont.cxx b/src/Font/Font_FTFont.cxx
index 5ae9899f..0a17372b 100644
--- a/src/Font/Font_FTFont.cxx
+13 -3
View File
@@ -17,10 +17,20 @@ else()
endif()
if(WIN32)
set(_conf_cmd perl Configure )
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
# OpenSSL's perl Configure honors the CC environment variable, but the
# VC-WIN64A makefile only works with cl (an unquoted clang-cl path with
# spaces, e.g. exported by CLion, silently produces no .obj files and the
# lib step fails with LNK1181). Pin the upstream toolchain.
# Keep rc.exe resolved from the MSVC developer environment as well. The
# absolute Windows SDK path contains spaces and OpenSSL 1.1.1 writes it to
# the generated nmake file without quoting, which skips .res generation.
# /FS serializes access to OpenSSL's shared generated PDB when cl is
# driven through nmake from a Ninja configure step.
set(_conf_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} perl Configure )
set(_cross_comp_prefix_line "")
set(_make_cmd nmake)
set(_install_cmd nmake install_sw )
set(_make_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake)
set(_install_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake install_sw )
else()
if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
+24
View File
@@ -1,3 +1,26 @@
# wxInspector finds wxWidgets through CMake's FindwxWidgets module, which only
# searches lib/vc*_lib because _WX_TOOL is hardcoded to "vc". A superbuild driven
# by clang-cl installs wxWidgets into lib/clang_x64_lib, so hand the module the
# directory wxWidgets actually used, derived the same way wxWidgetsConfig.cmake
# derives it.
set(_wxinspector_wx_hints "")
if (MSVC)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(_wx_compiler_prefix "clang")
else ()
set(_wx_compiler_prefix "vc")
endif ()
set(_wx_arch_suffix "")
if (CMAKE_GENERATOR_PLATFORM AND NOT CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
string(TOLOWER "_${CMAKE_GENERATOR_PLATFORM}" _wx_arch_suffix)
elseif (CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_wx_arch_suffix "_x64")
endif ()
set(_wxinspector_wx_hints
"-DwxWidgets_ROOT_DIR=${DESTDIR}"
"-DwxWidgets_LIB_DIR=${DESTDIR}/lib/${_wx_compiler_prefix}${_wx_arch_suffix}_lib")
endif ()
orcaslicer_add_cmake_project(
wxInspector
URL https://github.com/Noisyfox/wxInspector/archive/refs/tags/v1.0.0.zip
@@ -6,6 +29,7 @@ orcaslicer_add_cmake_project(
CMAKE_ARGS
-DCMAKE_CXX_FLAGS="-DwxDEBUG_LEVEL=0"
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
${_wxinspector_wx_hints}
)
if (MSVC)
-28
View File
@@ -1,28 +0,0 @@
---
build/cmake/wxWidgetsConfig.cmake.in | 10 +++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/build/cmake/wxWidgetsConfig.cmake.in b/build/cmake/wxWidgetsConfig.cmake.in
index 1a83f36..70ad8a4 100644
--- a/build/cmake/wxWidgetsConfig.cmake.in
+++ b/build/cmake/wxWidgetsConfig.cmake.in
@@ -58,7 +58,16 @@ if(WIN32_MSVC_NAMING)
endif()
endif()
-include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake")
+if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
+ if (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$")
+ set(_wx_clang_msvc_lib_dir "vc_arm64_lib")
+ else()
+ set(_wx_clang_msvc_lib_dir "vc_x64_lib")
+ endif()
+ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/${_wx_clang_msvc_lib_dir}/@PROJECT_NAME@Targets.cmake")
+else()
+ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake")
+endif()
macro(wx_inherit_property source dest name)
# property name without _<CONFIG>
--
2.43.0
-1
View File
@@ -28,7 +28,6 @@ orcaslicer_add_cmake_project(
GIT_SHALLOW ON
GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
PATCH_COMMAND git apply --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-Clang-CL-fix.patch
CMAKE_ARGS
-DwxBUILD_PRECOMP=ON
${_wx_toolkit}
@@ -0,0 +1,376 @@
# macOS FFmpeg Media Player Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make macOS use the same FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) as Windows/Linux, linking the static FFmpeg libraries from the deps build, and remove the old `wxMediaCtrl2.mm` BambuPlayer-based player.
**Architecture:** The new player is platform-neutral C++ already used on Linux/Windows. Enabling it on macOS is pure build wiring: compile `wxMediaCtrl3.cpp` + `AVVideoDecoder.cpp` on macOS, drop the `__WXMAC__` alias that redirects `wxMediaCtrl3` to the old `wxMediaCtrl2`, and link static FFmpeg (`libavcodec.a`/`libswscale.a`/`libavutil.a`) from the deps install. The Bambu stream API is dlsym'd at runtime from the network plugin (`libBambuSource.dylib`), which already exports it — no plugin changes needed. Rendering reuses the existing `wxImage` → `DrawBitmap` paint path (same as Linux).
**Tech Stack:** C++17, wxWidgets, CMake, FFmpeg 7.0.3 (libavcodec/libswscale/libavutil), macOS (Xcode generator), `deps/` ExternalProject build system.
## Global Constraints
- Branch: `dev/ffmpeg-player-macos`. Commit after every task.
- **Linux and Windows builds must not change** — the FFmpeg deps flag change is guarded by `APPLE`; Linux keeps `--enable-shared`, Windows keeps its prebuilt DLL zips.
- Static FFmpeg only on macOS: deps produce `libavcodec.a`/`libswscale.a`/`libavutil.a`; the app links those explicitly — the app binary must have **no** `libav*` dylib references (`otool -L` check).
- Follow existing code style: PascalCase classes, snake_case functions, C++17.
- No changes to `StatusPanel.cpp`, `MediaPlayCtrl.*`, or the BambuTunnel interface — the app already creates `wxMediaCtrl3` and uses only its public interface.
- The player cannot be unit-tested (hardware/plugin-dependent GUI code); verification is build-level, link-level, and manual runtime on a Mac.
- `localization/i18n/list.txt` references only `wxMediaCtrl2.cpp` (Win/Linux, stays) — no translation-list changes needed.
- Build dirs on the dev machine: main app = `build_arm64/` (Xcode generator, multi-config), deps = `deps/build/arm64/` (Unix Makefiles). App target name: `OrcaSlicer`. Substitute your own configured build dirs where noted.
---
### Task 1: Enable wxMediaCtrl3 on macOS and link static FFmpeg
**Files:**
- Modify: `src/slic3r/GUI/wxMediaCtrl3.h` (lines 18–22: the `#ifdef __WXMAC__` alias branch)
- Modify: `src/slic3r/GUI/wxMediaCtrl3.cpp:13` (uncomment the event define)
- Modify: `src/slic3r/GUI/wxMediaCtrl2.cpp:101` (remove the event define)
- Modify: `src/slic3r/CMakeLists.txt` (APPLE source list ~lines 779–792; FFmpeg link block ~lines 905–910)
**Interfaces:**
- Consumes: nothing new (all classes already exist).
- Produces: `wxMediaCtrl3` class compiled on macOS with the same interface as Linux/Windows — `Load(wxURI)`, `Play()`, `Stop()`, `SetIdleImage(wxString)`, `GetState()`, `GetLastError()`, `GetVideoSize()`, event `EVT_MEDIA_CTRL_STAT` defined once in the lib (from `wxMediaCtrl3.cpp`).
- [ ] **Step 1: Remove the macOS alias in wxMediaCtrl3.h**
Current (lines 16–23 of `src/slic3r/GUI/wxMediaCtrl3.h`):
```cpp
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#ifdef __WXMAC__
#include "wxMediaCtrl2.h"
#define wxMediaCtrl3 wxMediaCtrl2
#else
#define BAMBU_DYNAMIC
```
New:
```cpp
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#define BAMBU_DYNAMIC
```
Also remove the matching `#endif` that closed the `#else` branch (the one before the final `#endif /* wxMediaCtrl3_h */`), so the file's `#ifndef`/`#endif` guard pair stays balanced.
- [ ] **Step 2: Move the EVT_MEDIA_CTRL_STAT definition into wxMediaCtrl3.cpp**
In `src/slic3r/GUI/wxMediaCtrl3.cpp:13`, uncomment:
```cpp
//wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
```
becomes:
```cpp
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
```
In `src/slic3r/GUI/wxMediaCtrl2.cpp:101`, delete:
```cpp
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
```
(One definition total in the lib — `MediaPlayCtrl.cpp:59` binds this event on the media ctrl.)
- [ ] **Step 3: Update the APPLE source list in CMakeLists.txt**
In `src/slic3r/CMakeLists.txt`, the APPLE branch (currently compiles `wxMediaCtrl2.mm`, which becomes dead on macOS):
```cmake
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
GUI/wxMediaCtrl3.h
)
```
becomes:
```cmake
GUI/AVVideoDecoder.cpp
GUI/AVVideoDecoder.hpp
GUI/wxMediaCtrl3.cpp
GUI/wxMediaCtrl3.h
)
```
(The `else ()` branch — Win/Linux — stays exactly as it is.)
- [ ] **Step 4: Link static FFmpeg on macOS**
In `src/slic3r/CMakeLists.txt`, the FFmpeg block (currently `if (NOT APPLE)`):
```cmake
if (NOT APPLE)
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libswscale
libavutil
)
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
endif()
```
becomes:
```cmake
if (APPLE)
# Static FFmpeg from the deps install: nothing to bundle into the .app,
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil.
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libswscale
libavutil
)
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
endif()
```
The deps install (`${CMAKE_PREFIX_PATH}/lib`) already contains the three `.a` files from the existing arm64 deps build — no deps rebuild needed for this task.
- [ ] **Step 5: Reconfigure and build the app**
Run (Xcode generator; `cmake` re-runs automatically on build):
```bash
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
```
Expected: configure succeeds (no `pkg_check_modules` errors on macOS, `find_library` finds all three `.a` files), compile succeeds (`wxMediaCtrl3.cpp` and `AVVideoDecoder.cpp` compile on macOS without changes), link succeeds.
If CMake complains that `wxMediaCtrl3.h` is included but not in the source list or similar IDE-only warnings — ignore; headers in the list are cosmetic.
- [ ] **Step 6: Verify no dynamic FFmpeg dependency**
```bash
otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg"
```
Expected: prints `OK: no dynamic FFmpeg` (empty grep output). This is the whole point of static linking — nothing to bundle into the `.app`.
- [ ] **Step 7: Quick sanity — macOS unit tests still pass**
```bash
ctest --test-dir build_arm64/tests/libslic3r --output-on-failure
```
Expected: passes (add `-C RelWithDebInfo` if the multi-config generator requires it). If no tests were built in this build dir, build target `tests` first (`cmake --build build_arm64 --config RelWithDebInfo --target tests`).
- [ ] **Step 8: Commit**
```bash
git add src/slic3r/CMakeLists.txt src/slic3r/GUI/wxMediaCtrl3.h src/slic3r/GUI/wxMediaCtrl3.cpp src/slic3r/GUI/wxMediaCtrl2.cpp
git commit -m "feat: use FFmpeg media player on macOS with static FFmpeg"
```
---
### Task 2: Static-only FFmpeg in the macOS deps build
**Files:**
- Modify: `deps/FFMPEG/FFMPEG.cmake` (non-MSVC branch, APPLE section and CONFIGURE_COMMAND)
**Interfaces:**
- Consumes: nothing.
- Produces: a deps install on macOS containing only `libavcodec.a`, `libswscale.a`, `libavutil.a` (+ headers) — no `libav*` dylibs, so no bundling/rpath machinery is ever needed on macOS. Linux and Windows output are unchanged.
- [ ] **Step 1: Add the static flag variable**
In `deps/FFMPEG/FFMPEG.cmake`, inside the non-MSVC `else ()` branch, in the existing `if (APPLE)` block:
```cmake
if (APPLE)
set(_minos_cmd
"CFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}"
"LDFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}"
)
```
add after the `_minos_cmd` set:
```cmake
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
# Shared flags must come AFTER --enable-shared below so they win.
set(_link_cmd --enable-static --disable-shared)
```
and add a matching `else ()` after the `if (IS_CROSS_COMPILE) ... endif()` block inside that `if (APPLE)`, so non-Apple Unix keeps shared:
```cmake
else ()
set(_link_cmd --enable-shared)
endif ()
```
(If the existing `if (IS_CROSS_COMPILE)` block is the last thing inside `if (APPLE)`, the new `else ()` closes the `if (APPLE)` itself.)
- [ ] **Step 2: Use the variable in CONFIGURE_COMMAND**
In the `ExternalProject_Add(dep_FFMPEG ...)` configure command:
```cmake
"--prefix=${DESTDIR}"
--enable-shared
```
becomes:
```cmake
"--prefix=${DESTDIR}"
--enable-shared
${_link_cmd}
```
Order matters: `--enable-shared` comes first, then `--enable-static --disable-shared` (APPLE) or `--enable-shared` (Linux) — the last flag wins in FFmpeg configure.
- [ ] **Step 3: Rebuild the FFmpeg dep (slow — several minutes, run in background)**
The changed CONFIGURE_COMMAND invalidates the ExternalProject stamp, so this re-configures and rebuilds FFmpeg:
```bash
cmake --build deps/build/arm64 --target dep_FFMPEG
```
For a fully clean static-only check (removes the previous shared build tree, which can leave stale `.dylib` files behind in the in-source build):
```bash
rm -rf deps/build/arm64/dep_FFMPEG-prefix
cmake --build deps/build/arm64 --target dep_FFMPEG
```
- [ ] **Step 4: Verify the artifacts**
```bash
ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.a
ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.dylib 2>/dev/null || echo "OK: no dylibs"
```
Expected: `libavcodec.a` present, second command prints `OK: no dylibs`. Check `libavutil` and `libswscale` the same way.
- [ ] **Step 5: Verify the app still links against the static libs**
```bash
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg"
```
Expected: build succeeds, `OK: no dynamic FFmpeg`.
- [ ] **Step 6: Commit**
```bash
git add deps/FFMPEG/FFMPEG.cmake
git commit -m "build: build static-only FFmpeg for macOS deps"
```
---
### Task 3: Remove the old macOS player
**Files:**
- Delete: `src/slic3r/GUI/wxMediaCtrl2.mm`
- Delete: `src/slic3r/GUI/BambuPlayer/BambuPlayer.h` (and the empty `BambuPlayer/` dir)
- Modify: `src/slic3r/GUI/wxMediaCtrl2.h` (remove the `#ifdef __WXMAC__` section, lines 22–60)
**Interfaces:**
- Consumes: Task 1 (macOS no longer references `wxMediaCtrl2` — nothing includes `wxMediaCtrl2.h` on macOS anymore; `wxMediaCtrl2` is never instantiated on any platform).
- Produces: a clean tree where the old BambuPlayer-based player is gone from macOS. The `BambuPlayer` ObjC class itself remains inside the network plugin (external prebuilt binary) — only the GUI-side consumer is removed.
- [ ] **Step 1: Delete the old player files**
```bash
git rm src/slic3r/GUI/wxMediaCtrl2.mm
git rm src/slic3r/GUI/BambuPlayer/BambuPlayer.h
rmdir src/slic3r/GUI/BambuPlayer 2>/dev/null || true
```
- [ ] **Step 2: Strip the __WXMAC__ section from wxMediaCtrl2.h**
In `src/slic3r/GUI/wxMediaCtrl2.h`, remove the entire macOS branch of the `#ifdef __WXMAC__` guard — from `#ifdef __WXMAC__` (line 22) through the closing `};` of the mac class (line 60), and the `#else` marker — leaving only the non-mac `class wxMediaCtrl2 : public wxMediaCtrl { ... };` definition followed by the final `#endif /* wxMediaCtrl2_h */`. The resulting file keeps its `#ifndef`/`#endif` include guard pair balanced.
The file stays on disk because Win/Linux compile `wxMediaCtrl2.cpp`, which includes it.
- [ ] **Step 3: Grep for leftover references**
```bash
grep -rn "wxMediaCtrl2.mm\|BambuPlayer/BambuPlayer.h\|BambuPlayer" src/slic3r --include="*.cpp" --include="*.h" --include="*.mm" --include="*.txt"
```
Expected: no hits in `src/slic3r/GUI` (ignore `localization/i18n/list.txt:196`, which lists the Win/Linux `wxMediaCtrl2.cpp` and stays).
- [ ] **Step 4: Rebuild the app**
```bash
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
```
Expected: configure + compile + link succeed with the deleted files gone.
- [ ] **Step 5: Commit**
```bash
git add -A src/slic3r/GUI
git commit -m "refactor: remove old BambuPlayer-based media player from macOS"
```
---
### Task 4: Runtime verification on hardware
**Files:** none — manual verification.
**Interfaces:** consumes all prior tasks. Final gate: the new player must actually stream on a Mac.
- [ ] **Step 1: Launch the freshly built app**
```bash
open build_arm64/src/RelWithDebInfo/OrcaSlicer.app
```
Expected: app launches normally; no crash in the network/device subsystem.
- [ ] **Step 2: Load the network plugin and open the Device tab**
Log in / ensure the network plugin (`libBambuSource.dylib`) loads, select a printer, open the Device tab (camera monitoring panel).
Expected: the camera preview area shows the idle image initially (no crash — this exercises `wxMediaCtrl3::SetIdleImage` and the `wxImage` load path on macOS for the first time).
- [ ] **Step 3: Start the stream and watch it render**
Click play / wait for `MediaPlayCtrl` to start the stream.
Expected: live video renders in the panel. Check the console/log output (`BOOST_LOG` goes to the terminal if run from it, or check the log file):
- `stat_log ...` lines appear (the `EVT_MEDIA_CTRL_STAT` path is live — proves the Bambu C API dlsym worked from `libBambuSource.dylib`);
- no repeated decode/error messages like `AVVideoDecoder: ...` or `can not find function ...` (proves `StaticBambuLib::get` resolved all Bambu functions);
- Stop/Play toggle works; idle image reappears on stop;
- window resize keeps aspect ratio (exercises `DoSetSize`/`adjust_frame_size`/`paintEvent`).
- [ ] **Step 4: Confirm the old player is really gone**
Expected: nothing in the logs references `BambuPlayer` (the ObjC class is no longer dlsym'd); the video path is entirely `wxMediaCtrl3` + `AVVideoDecoder`.
If a printer is unavailable, at minimum verify Steps 1–2 (launch + idle image) and note in the PR that live-stream verification needs hardware.
- [ ] **Step 5: Final review pass**
```bash
git log --oneline -6
git show --stat HEAD # and each of the three task commits
```
Expected: the last 4 commits are the design doc + the 3 implementation tasks (each task commit touches only its listed files). Review the diff for scope: no Linux/Windows changes beyond the two `EVT_MEDIA_CTRL_STAT` lines in Task 1, no `StatusPanel`/`MediaPlayCtrl` changes.
@@ -0,0 +1,110 @@
# FFmpeg Media Player for macOS — Design
Date: 2026-08-14
Branch: `dev/ffmpeg-player-macos`
## Problem
The branch's new FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) is used on
Windows and Linux, but macOS still runs the old player: `wxMediaCtrl2.mm`, an ObjC
`BambuPlayer` class dlsym'd from the Bambu network plugin that renders via CALayer.
On macOS, `wxMediaCtrl3` is currently aliased to `wxMediaCtrl2` and FFmpeg is not linked
into the app at all.
Goal: make macOS use the same FFmpeg player as Windows/Linux, linking the **static**
FFmpeg libraries from the deps build instead of dynamic ones.
## Current state (verified)
- New player (Win/Linux): `GUI/wxMediaCtrl3.cpp` + `GUI/AVVideoDecoder.cpp`. Decodes with
FFmpeg (libavcodec/libswscale/libavutil), renders frames into `wxImage` (non-Windows) /
`wxBitmap` (Windows) drawn in a `paintEvent`, feeds via the `Bambu_*` C API
(`BambuTunnel.h`, `BAMBU_DYNAMIC`) dlsym'd from the network plugin through
`StaticBambuLib::get()` (`GUI/Printer/PrinterFileSystem.cpp`, compiled on all platforms).
- Old player (macOS): `GUI/wxMediaCtrl2.mm` uses the ObjC `BambuPlayer` class found via
`dlsym(module, "OBJC_CLASS_$_BambuPlayer")` in `libBambuSource.dylib`.
- The macOS network plugin `libBambuSource.dylib` already exports the full Bambu C API
(verified with `nm`), so the new player needs zero plugin changes.
- FFmpeg linking in `src/slic3r/CMakeLists.txt` is guarded by `if (NOT APPLE)` —
macOS currently does not link FFmpeg.
- `deps/FFMPEG/FFMPEG.cmake`: non-MSVC branch builds FFmpeg from source with
`--enable-shared`. The existing arm64 deps build on the dev machine happened to be
configured with both static and shared enabled, so `libavcodec.a` / `libswscale.a` /
`libavutil.a` are already present at
`deps/build/arm64/OrcaSlicer_dep/usr/local/lib/`.
- `EVT_MEDIA_CTRL_STAT` is `wxDEFINE_EVENT`'d in `wxMediaCtrl2.cpp` (Win/Linux) and
`wxMediaCtrl2.mm` (macOS); the define in `wxMediaCtrl3.cpp` is commented out.
- `wxMediaCtrl2` is never instantiated anywhere on any platform — dead code.
- `StatusPanel` already creates `wxMediaCtrl3`; `MediaPlayCtrl` only uses the
`wxMediaCtrl3` interface (`Load/Play/Stop/GetState/GetVideoSize/GetLastError/SetIdleImage`),
so no UI-side changes are needed.
## Approach (approved)
**Reuse the shared player on macOS.** Compile the existing `wxMediaCtrl3.cpp` +
`AVVideoDecoder.cpp` on macOS so all three platforms run one implementation.
Rendering uses the existing `wxImage` → `DrawBitmap` paint path, identical to Linux.
Known trade-off: frames are scaled to the widget's logical (1x) size, so Retina is
slightly soft compared to the old CALayer player. Accepted for now; a Retina-aware
scaling follow-up is possible later.
Rejected alternative: a native CGImage/CALayer renderer for macOS — faster and
Retina-crisp, but adds a second render implementation to maintain.
## Changes
### 1. Enable the FFmpeg player on macOS (source)
- `GUI/wxMediaCtrl3.h`: remove the `#ifdef __WXMAC__` branch (lines 18–22) that aliases
`wxMediaCtrl3` → `wxMediaCtrl2`. macOS then compiles the real `wxMediaCtrl3` class,
including the `BAMBU_DYNAMIC` BambuTunnel path used on Linux.
- Event symbol fix: move `wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent)` into
`wxMediaCtrl3.cpp` (uncomment the existing line) and remove it from
`wxMediaCtrl2.cpp`. One definition total in the lib; all three platforms resolve it.
### 2. Static FFmpeg linking (deps + app)
- `deps/FFMPEG/FFMPEG.cmake`: in the non-MSVC branch, pass
`--disable-shared --enable-static` when `APPLE`. Linux keeps `--enable-shared`;
Windows keeps its prebuilt shared DLL zips. Fresh macOS deps builds install only
`libavcodec.a` / `libswscale.a` / `libavutil.a` — no dylibs to bundle, no
rpath/install_name handling. (The existing local arm64 deps build already contains
the `.a` files, so no deps rebuild is strictly needed to try the change locally,
but a fresh CI deps build must produce them.)
- `src/slic3r/CMakeLists.txt`:
- APPLE branch of `SLIC3R_GUI_SOURCES`: add `GUI/wxMediaCtrl3.cpp`,
`GUI/wxMediaCtrl3.h`, `GUI/AVVideoDecoder.cpp`, `GUI/AVVideoDecoder.hpp`;
remove `GUI/wxMediaCtrl2.mm` and `GUI/wxMediaCtrl2.h` (the `.h` stays on
disk for the Win/Linux build of `wxMediaCtrl2.cpp`, but nothing on macOS
includes it after this change).
- Add an APPLE mirror of the `NOT APPLE` FFmpeg block: `find_library` for
`libavcodec.a`, `libswscale.a`, `libavutil.a` under `${CMAKE_PREFIX_PATH}/lib`
with `NO_DEFAULT_PATH`, link them (order avcodec → swscale → avutil), and add
`${CMAKE_PREFIX_PATH}/include` as a SYSTEM include directory. Deps are built with
`--disable-zlib` and no external codecs, so the three static libs link cleanly.
### 3. Remove the old player
- Delete `GUI/wxMediaCtrl2.mm` and `GUI/BambuPlayer/BambuPlayer.h` (header used only
by the `.mm`; the real `BambuPlayer` lives inside the network plugin).
- Remove the now-dead `__WXMAC__` section of `GUI/wxMediaCtrl2.h`.
- `wxMediaCtrl2.cpp` (Win/Linux) stays in the build as-is (dead but harmless; out of
scope to remove on this branch).
### 4. Verification
- Build on macOS: `cmake --build build_arm64` (or `build/arm64`).
- Confirm no dynamic FFmpeg dependency: `otool -L` on the app binary shows no `libav*`
dylib references.
- Runtime: with the network plugin loaded, the Device tab camera preview streams via
the FFmpeg player (check the device page / `MediaPlayCtrl`).
- macOS `ctest` still passes — static linking means no test-executable `.so` copying
hacks (unlike the Linux shared-lib setup).
## Out of scope
- Linux (shared libs, AppImage/flatpak bundling) and Windows (prebuilt DLL zips)
keep their current FFmpeg setup.
- Retina-aware frame scaling / native CGImage rendering (follow-up if visual quality
is judged insufficient).
- Audio streaming (neither player plays audio in this UI path).
-1
View File
@@ -193,7 +193,6 @@ src/slic3r/GUI/ObjColorDialog.cpp
src/slic3r/GUI/SyncAmsInfoDialog.cpp
src/slic3r/GUI/WipeTowerDialog.cpp
src/slic3r/GUI/wxExtensions.cpp
src/slic3r/GUI/wxMediaCtrl2.cpp
src/slic3r/GUI/WebUserLoginDialog.cpp
src/slic3r/GUI/WebGuideDialog.cpp
src/slic3r/GUI/KBShortcutsDialog.hpp
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4142,10 +4142,10 @@ msgid "PA Profile"
msgstr "Профіль PA"
msgid "Factor K"
msgstr "Коэф. K"
msgstr "Коеф. K"
msgid "Factor N"
msgstr "Коэф. N"
msgstr "Коеф. N"
msgid "Setting AMS slot information while printing is not supported"
msgstr "Зміна інформації про слоти AMS під час друку не підтримується"
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 579 KiB

@@ -20,6 +20,9 @@
"close_fan_the_first_x_layers": [
"3"
],
"during_print_exhaust_fan_speed": [
"0"
],
"fan_cooling_layer_time": [
"10"
],
@@ -20,6 +20,9 @@
"close_fan_the_first_x_layers": [
"3"
],
"during_print_exhaust_fan_speed": [
"0"
],
"fan_cooling_layer_time": [
"10"
],
@@ -20,6 +20,9 @@
"close_fan_the_first_x_layers": [
"3"
],
"during_print_exhaust_fan_speed": [
"0"
],
"fan_cooling_layer_time": [
"10"
],
@@ -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
@@ -312,6 +318,12 @@ modules:
sha256: 0ba163956f2d468b19a91b96c5aba66ee9610843ea41dda628ea44cdafde7db7
dest: external-packages/wxInspector
# FFmpeg n7.0.3
- type: file
url: https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc
dest: external-packages/FFMPEG
# ---------------------------------------------------------------
# Fallback archives for deps normally provided by the GNOME SDK.
# These are only used if find_package() fails to locate them.
+3
View File
@@ -25,6 +25,9 @@ export REQUIRED_DEV_PACKAGES=(
wayland-protocols
webkit2gtk-4.1
wget
pkgconf
yasm
nasm
)
if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -25,6 +25,9 @@ export REQUIRED_DEV_PACKAGES=(
wayland-protocols
webkit2gtk
wget
pkgconf
yasm
nasm
)
if [[ -n "$UPDATE_LIB" ]]
+1
View File
@@ -20,6 +20,7 @@ export REQUIRED_BUNDLES=(
perl-basic
texinfo
wget
nasm
)
if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -27,6 +27,9 @@ REQUIRED_DEV_PACKAGES=(
ninja-build
texinfo
wget
pkgconf
yasm
nasm
)
if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -31,6 +31,9 @@ REQUIRED_DEV_PACKAGES=(
webkit2gtk4.1-devel
wget
libcurl-devel
pkgconf
yasm
nasm
)
if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -32,6 +32,9 @@ REQUIRED_DEV_PACKAGES=(
sys-devel/m4
virtual/libudev
x11-libs/gtk+:3
dev-util/pkgconf
dev-lang/yasm
dev-lang/nasm
)
if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -30,6 +30,9 @@ REQUIRED_DEV_PACKAGES=(
webkit2gtk4-devel
wget
libcurl-devel
pkgconf
yasm
nasm
)
if [[ -n "$UPDATE_LIB" ]]
+15 -1
View File
@@ -75,7 +75,7 @@ if (SLIC3R_GUI)
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX expat)
list(APPEND wxWidgets_LIBRARIES ${EXPAT_LIBRARIES})
endif ()
# This is an issue in the new wxWidgets cmake build, doesn't deal with librt
find_library(LIBRT rt)
if(LIBRT)
@@ -294,6 +294,16 @@ if (WIN32)
endif()
else ()
if (NOT APPLE)
set(output_sos_Release "")
set(output_sos_Debug "")
add_custom_target(OrcaSlicerSosCopy ALL DEPENDS OrcaSlicer)
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
orcaslicer_copy_sos(OrcaSlicerSosCopy "Debug" "d" output_sos_Debug)
else()
orcaslicer_copy_sos(OrcaSlicerSosCopy "Release" "" output_sos_Release)
endif()
endif()
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
# On OSX, the name of the binary matches the name of the Application.
add_custom_command(TARGET OrcaSlicer POST_BUILD
@@ -372,5 +382,9 @@ if (WIN32)
install(FILES ${output_dlls_${build_type}} DESTINATION ".")
install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python")
else ()
if (APPLE)
else()
install(FILES ${output_sos_${build_type}} DESTINATION "${CMAKE_INSTALL_PREFIX}")
endif()
install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
+2
View File
@@ -3,7 +3,9 @@
#define _WIN32_WINNT 0x0502
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <wchar.h>
#include <commctrl.h>
+2
View File
@@ -2,7 +2,9 @@
#define _WIN32_WINNT 0x0502
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <shellapi.h>
#include <wchar.h>
@@ -83,6 +83,10 @@ copy_shared_object_to_dir() {
src_real="$(readlink -f "$src")"
dst_name="$(basename "$src_real")"
mkdir -p "$dst_dir"
if [ "$src_real" = "$dst_dir/$dst_name" ]; then
# Already bundled; the dependency resolved from the bundle directory.
return 0
fi
cp -fL "$src_real" "$dst_dir/$dst_name"
if [ -L "$src" ]; then
@@ -96,12 +100,23 @@ copy_shared_object_to_dir() {
}
bundle_dependency_closure() {
local dst_dir="$1"
local dst_dir
dst_dir="$(cd -- "$1" && pwd)"
shift
local -a queue=("$@")
local target dep dep_real copied_path
local target dep dep_real dep_key copied_path
declare -A seen=()
# Dependencies are resolved with ldd, which only searches the default
# loader path. Deps-built shared libraries (e.g. the FFmpeg stack) are not
# installed there and carry no RUNPATH of their own, so once copied into
# the bundle ldd can no longer resolve one sibling from another
# (libavcodec -> libavutil) and reports it as missing. Extend the loader
# path with the bundle directory plus the source directories of files
# already bundled, so every library that was resolved once keeps resolving
# for its own dependencies. The audit script does the same
# (scripts/check_appimage_libs.sh).
local -a search_dirs=("$dst_dir")
while [ ${#queue[@]} -gt 0 ]; do
target="${queue[0]}"
@@ -122,17 +137,24 @@ bundle_dependency_closure() {
continue
fi
if [ -n "${seen[$dep_real]}" ]; then
# Key dedup on the bundled file rather than the source path: once
# ldd resolves a library from the bundle directory (via the
# LD_LIBRARY_PATH above) its path is a dst_dir path, which differs
# from the source path the first resolution returned. Keying on
# the source path would re-copy the file onto itself.
dep_key="$dst_dir/$(basename "$dep_real")"
if [ -n "${seen[$dep_key]}" ]; then
continue
fi
seen[$dep_real]=1
seen[$dep_key]=1
copy_shared_object_to_dir "$dep" "$dst_dir"
search_dirs+=("$(dirname "$dep_real")")
copied_path="$dst_dir/$(basename "$dep_real")"
if [ -e "$copied_path" ]; then
queue+=("$copied_path")
fi
done < <(appimage_list_direct_dependencies "$target")
done < <(LD_LIBRARY_PATH="$(IFS=:; printf '%s' "${search_dirs[*]}")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" appimage_list_direct_dependencies "$target")
done
}
+1 -1
View File
@@ -229,7 +229,7 @@ public:
m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {}
size_t idx() const { return m_idx; }
const BoundingBox& bbox() const { return m_bbox; }
Point centroid() const { return (m_bbox.min() + m_bbox.max() / 2); }
Point centroid() const { return (m_bbox.min() + m_bbox.max()) / 2; }
private:
size_t m_idx;
BoundingBox m_bbox;
+2 -2
View File
@@ -154,8 +154,8 @@ void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const
//h^2 = L^2 / b^2 [factor the divisor]
const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2);
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) //Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current, previous, next) <= scaled<double>(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) //Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current, previous, next) <= double(colinear_vertex_tolerance()))) // make sure that height_2 is not small because of cancellation of positive and negative areas
continue;
if (length2 < smallest_line_segment_squared
@@ -133,8 +133,8 @@ void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const
const auto height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2));
const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next);
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) // Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= scaled<double>(0.005)) // Make sure that height_2 is not small because of cancellation of positive and negative areas
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) // Almost exactly colinear (barring rounding errors).
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= double(colinear_vertex_tolerance())) // Make sure that height_2 is not small because of cancellation of positive and negative areas
// We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed
&& extrusion_area_error <= maximum_extrusion_area_deviation)
{
@@ -32,6 +32,14 @@ class Flow;
namespace Slic3r::Arachne
{
// ORCA: Tolerance of the "almost exactly colinear" early-out shared by the two simplify() passes
// (this file and WallToolPaths.cpp). That test drops a vertex regardless of the user's Maximum wall
// resolution/deviation, so it has to stay at the scale of coordinate rounding noise. A larger value
// silently decimates finely tessellated curves: on a circle, one vertex may be removed whenever the
// sagitta of the resulting chord falls below the tolerance, which halves the point count and turns
// smooth arcs into corners the firmware has to decelerate through.
inline coord_t colinear_vertex_tolerance() { return coord_t(SCALED_EPSILON); }
/*!
* Represents a polyline (not just a line) that is to be extruded with variable
* line width.
+6 -6
View File
@@ -57,24 +57,24 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData
//BBS: can be fit as arc, then save arc data temperarily
last_arc = target_arc;
if (back_index == points.size() - 1) {
result.emplace_back(std::move(PathFittingData{ front_index,
result.emplace_back(PathFittingData{ front_index,
back_index,
last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw,
last_arc }));
last_arc });
front_index = back_index;
}
} else {
if (back_index - front_index > 2) {
//BBS: althought current point_stack can't be fit as arc,
//but previous must can be fit if removing the top in stack, so save last arc
result.emplace_back(std::move(PathFittingData{ front_index,
result.emplace_back(PathFittingData{ front_index,
back_index - 1,
last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw,
last_arc }));
last_arc });
} else {
//BBS: save the first segment as line move when 3 point-line can't be fit as arc move
if (result.empty() || result.back().path_type != EMovePathType::Linear_move)
result.emplace_back(std::move(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()}));
result.emplace_back(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()});
else if(result.back().path_type == EMovePathType::Linear_move)
result.back().end_point_index = front_index + 1;
}
@@ -87,7 +87,7 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData
//BBS: handle the remain data
if (front_index != back_index) {
if (result.empty() || result.back().path_type != EMovePathType::Linear_move)
result.emplace_back(std::move(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()}));
result.emplace_back(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()});
else if (result.back().path_type == EMovePathType::Linear_move)
result.back().end_point_index = back_index;
}
+17 -1
View File
@@ -179,6 +179,17 @@ set(lisbslic3r_sources
Fill/Lightning/Layer.hpp
Fill/Lightning/TreeNode.cpp
Fill/Lightning/TreeNode.hpp
FilamentMixer.cpp
FilamentMixer.hpp
FilamentMixerModel.hpp
ColorDecomposeRecipe.cpp
ColorDecomposeRecipe.hpp
TexturePainting.hpp
TexturePainting.cpp
TextureToColor/TextureToColor.hpp
TextureToColor/TextureToColor.cpp
TextureToColor/ColorUtils.hpp
TextureToColor/ColorUtils.cpp
Flow.cpp
Flow.hpp
FlushVolCalc.cpp
@@ -194,6 +205,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
@@ -509,6 +523,7 @@ cmake_policy(SET CMP0011 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0167 NEW)
find_package(CGAL REQUIRED)
find_package(OpenCV REQUIRED core)
find_package(assimp REQUIRED)
unset(CMAKE_POLICY_DEFAULT_CMP0167)
cmake_policy(POP)
@@ -549,7 +564,7 @@ target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTI
if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif()
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/TextureToColor PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS})
# Find the OCCT and related libraries
@@ -597,6 +612,7 @@ target_link_libraries(libslic3r
libnest2d
miniz
opencv_world
assimp::assimp
PRIVATE
${CMAKE_DL_LIBS}
${EXPAT_LIBRARIES}
+8 -8
View File
@@ -13,8 +13,8 @@ Slic3r::Polylines Paths64_to_polylines(const Clipper2Lib::Paths64& in)
Slic3r::Points points;
points.reserve(path64.size());
for (const Clipper2Lib::Point64& point64 : path64)
points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y)));
out.emplace_back(std::move(Slic3r::Polyline(points)));
points.emplace_back(Slic3r::Point(point64.x, point64.y));
out.emplace_back(Slic3r::Polyline(points));
}
return out;
}
@@ -29,7 +29,7 @@ Clipper2Lib::Paths64 Slic3rPoints_to_Paths64(const Container& in)
Clipper2Lib::Path64 path;
path.reserve(item.size());
for (const Slic3r::Point& point : item.points)
path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
out.emplace_back(std::move(path));
}
return out;
@@ -44,7 +44,7 @@ Points Path64ToPoints(const Clipper2Lib::Path64& path64)
{
Points points;
points.reserve(path64.size());
for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y)));
for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(Slic3r::Point(point64.x, point64.y));
return points;
}
@@ -99,7 +99,7 @@ Clipper2Lib::Paths64 Slic3rPolygons_to_Paths64(const Polygons &in)
for (const Polygon &poly : in) {
Clipper2Lib::Path64 path;
path.reserve(poly.points.size());
for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
out.emplace_back(std::move(path));
}
return out;
@@ -114,7 +114,7 @@ Clipper2Lib::Paths64 Slic3rExPolygons_to_Paths64(const ExPolygons& in)
const auto &poly = expolygon.contour_or_hole(i);
Clipper2Lib::Path64 path;
path.reserve(poly.points.size());
for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
out.emplace_back(std::move(path));
}
}
@@ -134,8 +134,8 @@ Polylines _clipper2_pl_open(Clipper2Lib::ClipType clipType, const Slic3r::Polyli
Slic3r::Polylines out;
out.reserve(solution.size() + solution_open.size());
polylines_append(out, std::move(Paths64_to_polylines(solution)));
polylines_append(out, std::move(Paths64_to_polylines(solution_open)));
polylines_append(out, Paths64_to_polylines(solution));
polylines_append(out, Paths64_to_polylines(solution_open));
return out;
}
+530
View File
@@ -0,0 +1,530 @@
#include "ColorDecomposeRecipe.hpp"
#include "FilamentMixer.hpp"
#include "Utils.hpp"
#include "nlohmann/json.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <limits>
#include <utility>
namespace Slic3r {
namespace {
struct LabColor {
double l{0.0};
double a{0.0};
double b{0.0};
};
struct StandardRecipeEntry {
ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::CMYW};
std::string material;
std::string source;
std::vector<std::string> component_keys;
std::vector<std::string> component_hexes;
std::vector<int> ratios;
std::string measured_hex;
LabColor measured_lab;
};
static double srgb_to_linear(double v)
{
v /= 255.0;
return v <= 0.04045 ? v / 12.92 : std::pow((v + 0.055) / 1.055, 2.4);
}
static double xyz_to_lab_component(double v)
{
constexpr double eps = 216.0 / 24389.0;
constexpr double kappa = 24389.0 / 27.0;
return v > eps ? std::cbrt(v) : (kappa * v + 16.0) / 116.0;
}
static LabColor rgb_to_lab(const ColorDecomposeRgb& rgb)
{
const double r = srgb_to_linear(rgb.r);
const double g = srgb_to_linear(rgb.g);
const double b = srgb_to_linear(rgb.b);
const double x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
const double y = (0.2126729 * r + 0.7151522 * g + 0.0721750 * b);
const double z = (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / 1.08883;
const double fx = xyz_to_lab_component(x);
const double fy = xyz_to_lab_component(y);
const double fz = xyz_to_lab_component(z);
return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)};
}
static std::string lab_to_srgb_hex(const LabColor& lab)
{
constexpr double Xn = 0.95047, Yn = 1.0, Zn = 1.08883;
auto f_inv = [](double t) -> double {
constexpr double eps = 216.0 / 24389.0;
constexpr double kappa = 24389.0 / 27.0;
const double t3 = t * t * t;
return t3 > eps ? t3 : (t * 116.0 - 16.0) / kappa;
};
const double fy = (lab.l + 16.0) / 116.0;
const double fx = lab.a / 500.0 + fy;
const double fz = fy - lab.b / 200.0;
const double X = Xn * f_inv(fx);
const double Y = Yn * f_inv(fy);
const double Z = Zn * f_inv(fz);
double r = 3.2406 * X - 1.5372 * Y - 0.4986 * Z;
double g = -0.9689 * X + 1.8758 * Y + 0.0415 * Z;
double b = 0.0557 * X - 0.2040 * Y + 1.0570 * Z;
auto gamma = [](double c) -> double {
c = std::max(0.0, std::min(1.0, c));
return c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055;
};
auto u8 = [&](double c) -> int {
return std::max(0, std::min(255, static_cast<int>(std::lround(gamma(c) * 255.0))));
};
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", u8(r), u8(g), u8(b));
return std::string(buf);
}
static double delta_e76(const LabColor& a, const LabColor& b)
{
return std::sqrt(std::pow(a.l - b.l, 2.0) + std::pow(a.a - b.a, 2.0) + std::pow(a.b - b.b, 2.0));
}
static bool material_matches(const std::string& a, const std::string& b)
{
if (a.empty() || b.empty())
return false;
return a == b || a == b + " Basic" || b == a + " Basic";
}
static std::vector<std::vector<int>> ratio_grid(size_t n)
{
std::vector<std::vector<int>> out;
if (n == 2) {
for (int a = 20; a <= 80; a += 5)
out.push_back({a, 100 - a});
} else if (n == 3) {
for (int a = 20; a <= 60; a += 5)
for (int b = 20; b <= 80 - a; b += 5) {
const int c = 100 - a - b;
if (c >= 20)
out.push_back({a, b, c});
}
}
return out;
}
static ColorDecomposeRecipeMode parse_mode(const std::string& s)
{
if (s == "RYBW" || s == "RGBY")
return ColorDecomposeRecipeMode::RYBW;
return ColorDecomposeRecipeMode::CMYW;
}
static std::vector<StandardRecipeEntry> load_standard_entries()
{
std::vector<StandardRecipeEntry> entries;
const std::string path = resources_dir() + "/filament_mixing/standard_color_recipes.json";
std::ifstream ifs(path);
if (!ifs)
return entries;
nlohmann::json root = nlohmann::json::parse(ifs, nullptr, false);
if (root.is_discarded() || !root.contains("entries") || !root["entries"].is_array())
return entries;
for (const auto& item : root["entries"]) {
if (!item.is_object())
continue;
StandardRecipeEntry entry;
entry.mode = parse_mode(item.value("mode", "CMYW"));
entry.material = item.value("material", "");
entry.source = item.value("source", "");
entry.measured_hex = item.value("measured_rgb", "");
if (item.contains("components") && item["components"].is_array()) {
for (const auto& comp : item["components"]) {
if (comp.is_object()) {
entry.component_keys.push_back(comp.value("key", ""));
entry.component_hexes.push_back(comp.value("rgb", ""));
}
}
}
if (item.contains("ratios") && item["ratios"].is_array()) {
for (const auto& ratio : item["ratios"]) {
if (ratio.is_number_integer())
entry.ratios.push_back(ratio.get<int>());
}
}
if (item.contains("measured_lab") && item["measured_lab"].is_array() && item["measured_lab"].size() >= 3) {
entry.measured_lab = {
item["measured_lab"][0].get<double>(),
item["measured_lab"][1].get<double>(),
item["measured_lab"][2].get<double>()
};
} else {
ColorDecomposeRgb measured_rgb;
if (!color_decompose_hex_to_rgb(entry.measured_hex, measured_rgb))
continue;
entry.measured_lab = rgb_to_lab(measured_rgb);
}
if (entry.component_hexes.size() >= 2 && entry.component_hexes.size() == entry.ratios.size() &&
!entry.measured_hex.empty())
entries.push_back(std::move(entry));
}
return entries;
}
static const std::vector<StandardRecipeEntry>& standard_entries()
{
static const std::vector<StandardRecipeEntry> entries = load_standard_entries();
return entries;
}
static void evaluate_candidate(const ColorDecomposeRgb& target,
const std::vector<std::string>& hexes,
const std::vector<int>& ratios,
const std::vector<unsigned int>& indices,
ColorDecomposeRecipeMode mode,
double& best_score,
ColorDecomposeRecipeResult& best)
{
const std::string mixed = blend_color_multi(hexes, ratios);
ColorDecomposeRgb mixed_rgb;
if (!color_decompose_hex_to_rgb(mixed, mixed_rgb))
return;
const double score = delta_e76(rgb_to_lab(target), rgb_to_lab(mixed_rgb));
if (score >= best_score)
return;
best_score = score;
best.valid = true;
best.mode = mode;
best.matched_color_hex = mixed;
best.components.clear();
for (size_t i = 0; i < hexes.size(); ++i) {
ColorDecomposeRecipeComponent comp;
comp.color_hex = hexes[i];
comp.ratio = ratios[i];
comp.filament_index = i < indices.size() ? indices[i] : 0;
best.components.push_back(comp);
}
}
} // namespace
std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb)
{
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
return std::string(buf);
}
bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out)
{
if (hex.size() < 7 || hex[0] != '#')
return false;
unsigned r = 0, g = 0, b = 0;
if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &r, &g, &b) != 3)
return false;
out = {static_cast<unsigned char>(r), static_cast<unsigned char>(g), static_cast<unsigned char>(b)};
return true;
}
ColorDecomposeRecipeResult recommend_from_physical_filaments(
const ColorDecomposeRgb& target,
const std::vector<ColorDecomposePhysicalFilament>& physical_filaments,
const std::string& preferred_material_type)
{
std::vector<ColorDecomposePhysicalFilament> candidates;
for (const auto& filament : physical_filaments) {
if (filament.is_mixed)
continue;
ColorDecomposeRgb ignored;
if (!color_decompose_hex_to_rgb(filament.color_hex, ignored))
continue;
if (preferred_material_type.empty() || material_matches(filament.type, preferred_material_type))
candidates.push_back(filament);
}
// Early exit: if a material-matched candidate has the exact target color,
// return it as 100%. Downstream rejects single-component results (no mixed
// slot created), which is correct -- the color already exists.
const std::string target_hex = color_decompose_rgb_to_hex(target);
for (const auto& cand : candidates) {
ColorDecomposeRgb cand_rgb;
if (!color_decompose_hex_to_rgb(cand.color_hex, cand_rgb))
continue;
if (color_decompose_rgb_to_hex(cand_rgb) == target_hex) {
ColorDecomposeRecipeResult exact;
exact.valid = true;
exact.mode = ColorDecomposeRecipeMode::MaterialList;
exact.matched_color_hex = cand.color_hex;
ColorDecomposeRecipeComponent comp;
comp.color_hex = cand.color_hex;
comp.ratio = 100;
comp.filament_index = cand.filament_index;
exact.components.push_back(comp);
return exact;
}
}
if (candidates.size() < 2)
candidates = physical_filaments;
candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [](const auto& filament) {
if (filament.is_mixed)
return true;
ColorDecomposeRgb ignored;
return !color_decompose_hex_to_rgb(filament.color_hex, ignored);
}), candidates.end());
constexpr size_t kMaxCandidates = 8;
if (candidates.size() > kMaxCandidates) {
const LabColor target_lab = rgb_to_lab(target);
std::sort(candidates.begin(), candidates.end(),
[&target_lab](const ColorDecomposePhysicalFilament& a, const ColorDecomposePhysicalFilament& b) {
ColorDecomposeRgb rgb_a, rgb_b;
color_decompose_hex_to_rgb(a.color_hex, rgb_a);
color_decompose_hex_to_rgb(b.color_hex, rgb_b);
return delta_e76(target_lab, rgb_to_lab(rgb_a))
< delta_e76(target_lab, rgb_to_lab(rgb_b));
});
candidates.resize(kMaxCandidates);
}
ColorDecomposeRecipeResult best;
double best_score = std::numeric_limits<double>::max();
for (size_t i = 0; i < candidates.size(); ++i) {
for (size_t j = i + 1; j < candidates.size(); ++j) {
const std::vector<std::string> hexes = {candidates[i].color_hex, candidates[j].color_hex};
const std::vector<unsigned int> indices = {candidates[i].filament_index, candidates[j].filament_index};
for (const auto& ratios : ratio_grid(2))
evaluate_candidate(target, hexes, ratios, indices, ColorDecomposeRecipeMode::MaterialList, best_score, best);
for (size_t k = j + 1; k < candidates.size(); ++k) {
const std::vector<std::string> hexes3 = {candidates[i].color_hex, candidates[j].color_hex, candidates[k].color_hex};
const std::vector<unsigned int> indices3 = {candidates[i].filament_index, candidates[j].filament_index, candidates[k].filament_index};
for (const auto& ratios : ratio_grid(3))
evaluate_candidate(target, hexes3, ratios, indices3, ColorDecomposeRecipeMode::MaterialList, best_score, best);
}
}
}
return best;
}
ColorDecomposeRecipeResult lookup_standard_recipe(
const ColorDecomposeRgb& target,
ColorDecomposeRecipeMode mode,
const std::string& preferred_material_type)
{
const LabColor target_lab = rgb_to_lab(target);
ColorDecomposeRecipeResult best;
double best_score = std::numeric_limits<double>::max();
auto consider = [&](bool require_material_match) {
for (const StandardRecipeEntry& entry : standard_entries()) {
if (entry.mode != mode)
continue;
if (require_material_match && !material_matches(entry.material, preferred_material_type))
continue;
if (!require_material_match && !preferred_material_type.empty() && material_matches(entry.material, preferred_material_type))
continue;
const double score = delta_e76(target_lab, entry.measured_lab);
if (score >= best_score)
continue;
best_score = score;
best.valid = true;
best.mode = mode;
best.matched_color_hex = entry.measured_hex;
best.components.clear();
for (size_t i = 0; i < entry.component_hexes.size(); ++i) {
ColorDecomposeRecipeComponent comp;
comp.color_hex = entry.component_hexes[i];
comp.base_color = i < entry.component_keys.size() ? entry.component_keys[i] : "";
comp.ratio = entry.ratios[i];
comp.filament_index = 0;
best.components.push_back(comp);
}
}
};
consider(true);
if (!best.valid)
consider(false);
return best;
}
std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes,
const std::vector<int>& ratios)
{
if (component_hexes.size() < 2 || component_hexes.size() != ratios.size())
return {};
auto normalize_hex = [](const std::string& hex) -> std::string {
ColorDecomposeRgb rgb;
if (!color_decompose_hex_to_rgb(hex, rgb))
return {};
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
return std::string(buf);
};
// Stage 1: canonicalize input by sorting (hex, ratio) pairs so matching
// is independent of the caller's component order.
const size_t n = component_hexes.size();
std::vector<std::pair<std::string, int>> in_pairs;
in_pairs.reserve(n);
for (size_t i = 0; i < n; ++i) {
std::string nh = normalize_hex(component_hexes[i]);
if (nh.empty())
return {};
in_pairs.emplace_back(std::move(nh), ratios[i]);
}
std::sort(in_pairs.begin(), in_pairs.end());
std::vector<std::string> in_hexes;
std::vector<int> in_ratios;
in_hexes.reserve(n);
in_ratios.reserve(n);
for (const auto& p : in_pairs) {
in_hexes.push_back(p.first);
in_ratios.push_back(p.second);
}
// Normalize ratios to sum=100 (callers may pass arbitrary weights,
// e.g. MixedFilamentDialog uses ratio*10000).
{
int sum = 0;
for (int r : in_ratios) sum += r;
if (sum > 0 && sum != 100) {
int new_sum = 0;
for (size_t i = 0; i < in_ratios.size(); ++i) {
in_ratios[i] = static_cast<int>(std::lround(
static_cast<double>(in_ratios[i]) * 100.0 / static_cast<double>(sum)));
new_sum += in_ratios[i];
}
if (new_sum != 100) {
auto it = std::max_element(in_ratios.begin(), in_ratios.end());
*it += (100 - new_sum);
}
}
}
// Fall back to polynomial model for ratios outside the measured range.
{
bool out_of_range = false;
if (n == 2) {
for (int r : in_ratios)
if (r < 20 || r > 80) { out_of_range = true; break; }
} else {
for (int r : in_ratios)
if (r < 20) { out_of_range = true; break; }
}
if (out_of_range)
return {};
}
// Stage 2: collect anchors with the same component hex set; try exact match.
struct Anchor {
std::vector<int> ratios;
LabColor lab;
std::string hex;
};
std::vector<Anchor> anchors;
for (const StandardRecipeEntry& entry : standard_entries()) {
if (entry.source != "measured" && entry.source != "interpolated")
continue;
if (entry.component_hexes.size() != n)
continue;
std::vector<std::pair<std::string, int>> e_pairs;
e_pairs.reserve(n);
for (size_t i = 0; i < n; ++i)
e_pairs.emplace_back(normalize_hex(entry.component_hexes[i]), entry.ratios[i]);
std::sort(e_pairs.begin(), e_pairs.end());
bool same_set = true;
for (size_t i = 0; i < n; ++i)
if (e_pairs[i].first != in_hexes[i]) { same_set = false; break; }
if (!same_set)
continue;
Anchor a;
a.ratios.reserve(n);
for (const auto& p : e_pairs) a.ratios.push_back(p.second);
a.lab = entry.measured_lab;
a.hex = entry.measured_hex;
if (a.ratios == in_ratios)
return a.hex;
anchors.push_back(std::move(a));
}
if (anchors.size() < 2)
return {};
// Stage 3: interpolation in Lab space.
if (n == 2) {
// 1D linear interpolation along ratio[0].
std::sort(anchors.begin(), anchors.end(),
[](const Anchor& a, const Anchor& b) { return a.ratios[0] < b.ratios[0]; });
const double x = static_cast<double>(in_ratios[0]);
size_t lo = 0;
while (lo + 2 < anchors.size() && static_cast<double>(anchors[lo + 1].ratios[0]) <= x)
++lo;
const Anchor& a0 = anchors[lo];
const Anchor& a1 = anchors[lo + 1];
const double span = static_cast<double>(a1.ratios[0] - a0.ratios[0]);
const double t = span > 0.0 ? (x - static_cast<double>(a0.ratios[0])) / span : 0.0;
return lab_to_srgb_hex({a0.lab.l + t * (a1.lab.l - a0.lab.l),
a0.lab.a + t * (a1.lab.a - a0.lab.a),
a0.lab.b + t * (a1.lab.b - a0.lab.b)});
}
// 3+ color: IDW (p=2) with 3 nearest anchors in the (ratio[0], ratio[1]) plane.
const double ra = static_cast<double>(in_ratios[0]);
const double rb = static_cast<double>(in_ratios[1]);
std::vector<std::pair<double, const Anchor*>> dists;
dists.reserve(anchors.size());
for (const Anchor& a : anchors) {
const double d = std::sqrt(std::pow(ra - static_cast<double>(a.ratios[0]), 2.0) +
std::pow(rb - static_cast<double>(a.ratios[1]), 2.0));
if (d == 0.0)
return a.hex;
dists.emplace_back(d, &a);
}
const size_t k = std::min(static_cast<size_t>(3), dists.size());
std::partial_sort(dists.begin(), dists.begin() + k, dists.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
double num_l = 0.0, num_a = 0.0, num_b = 0.0, den = 0.0;
for (size_t j = 0; j < k; ++j) {
const double w = 1.0 / (dists[j].first * dists[j].first);
num_l += w * dists[j].second->lab.l;
num_a += w * dists[j].second->lab.a;
num_b += w * dists[j].second->lab.b;
den += w;
}
return lab_to_srgb_hex({num_l / den, num_a / den, num_b / den});
}
} // namespace Slic3r
+64
View File
@@ -0,0 +1,64 @@
#ifndef SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP
#define SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP
#include <string>
#include <vector>
namespace Slic3r {
enum class ColorDecomposeRecipeMode {
MaterialList,
CMYW,
RYBW
};
struct ColorDecomposeRgb {
unsigned char r{0};
unsigned char g{0};
unsigned char b{0};
};
struct ColorDecomposePhysicalFilament {
std::string color_hex;
std::string name;
std::string type;
bool is_mixed{false};
unsigned int filament_index{0}; // 1-based physical filament index
};
struct ColorDecomposeRecipeComponent {
std::string color_hex;
std::string base_color;
int ratio{0};
unsigned int filament_index{0}; // 1-based for physical filaments, 0 for standard base colors
};
struct ColorDecomposeRecipeResult {
bool valid{false};
ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::MaterialList};
std::string matched_color_hex;
std::vector<ColorDecomposeRecipeComponent> components;
};
std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb);
bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out);
ColorDecomposeRecipeResult recommend_from_physical_filaments(
const ColorDecomposeRgb& target,
const std::vector<ColorDecomposePhysicalFilament>& physical_filaments,
const std::string& preferred_material_type);
ColorDecomposeRecipeResult lookup_standard_recipe(
const ColorDecomposeRgb& target,
ColorDecomposeRecipeMode mode,
const std::string& preferred_material_type);
// Look up the measured blend color for an exact (component_hexes, ratios) match
// in the standard color recipe table. Returns the measured hex color if found
// with reliable source data ("measured" or "interpolated"), empty string otherwise.
std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes,
const std::vector<int>& ratios);
} // namespace Slic3r
#endif // SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP
+2 -1
View File
@@ -2031,7 +2031,8 @@ const double& DynamicConfig::opt_float(const t_config_option_key &opt_key, unsig
return opt_floats_nullable->get_at(idx);
} else {
assert(false);
return 0;
static const double zero = 0.0;
return zero;
}
}
+14 -5
View File
@@ -682,7 +682,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
return fuzzified;
}
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour)
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed)
{
const auto slice_z = perimeter_generator.slice_z;
const auto& regions = perimeter_generator.regions_by_fuzzify;
@@ -690,7 +690,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
if (fuzzify)
fuzzy_extrusion_line(extrusion->junctions, slice_z, config);
fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed);
} else {
// Merge regions that produce identical fuzzy effects (differ only in type).
// When the style (e.g. External) and a painted region (All) both fuzzify this loop
@@ -701,10 +701,19 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
// Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config);
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed);
return;
}
// Open path means this is a thin wall that collapsed into a single thick line, in this case the path will go exactly
// between the middle two sides of the object. And since the paint segmentation never goes beyond the middle line because
// it uses voronoi diagram, we need to expand the segmentation a little bit to make sure it covers the path.
if (!closed) {
for (auto& r : merged_regions) {
r.expolygons = offset_ex(r.expolygons, perimeter_generator.ext_perimeter_flow.scaled_width() / 10);
}
}
#ifdef DEBUG_FUZZY
{
int i = 0;
@@ -752,7 +761,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
// Fuzzy splitted extrusion
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
// The entire polygon is fuzzified
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config);
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed);
continue;
} else {
const auto current_ext = extrusion->junctions;
@@ -803,7 +812,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
}
//Orca: ensure the loop is closed after fuzzy
if (!extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
if (closed && !extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
extrusion->junctions.back().p = extrusion->junctions.front().p;
extrusion->junctions.back().w = extrusion->junctions.front().w;
}
@@ -16,7 +16,7 @@ void group_region_by_fuzzify(PerimeterGenerator& g);
bool should_fuzzify(const FuzzySkinConfig& config, int layer_id, size_t loop_idx, bool is_contour);
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, size_t loop_idx, bool is_contour);
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour);
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour, bool closed = true);
} // namespace Slic3r::Feature::FuzzySkin
+1 -1
View File
@@ -1021,7 +1021,7 @@ namespace Slic3r
if (FGMode::MatchMode == ctx.group_info.mode)
return calc_filament_group_for_match(cost);
}
catch (const FilamentGroupException& e) {
catch (const FilamentGroupException&) {
}
return calc_filament_group_for_flush(cost);
+829
View File
@@ -0,0 +1,829 @@
#include "FilamentMixer.hpp"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <limits>
#include <set>
#include <sstream>
#include <numeric>
#include <boost/log/trivial.hpp>
#include "ColorDecomposeRecipe.hpp"
#include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp"
namespace Slic3r {
namespace {
inline float clamp01(float x)
{
return std::max(0.0f, std::min(1.0f, x));
}
inline float srgb_to_linear(float x)
{
return (x >= 0.04045f) ? std::pow((x + 0.055f) / 1.055f, 2.4f) : x / 12.92f;
}
inline float linear_to_srgb(float x)
{
return (x >= 0.0031308f) ? (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f) : (12.92f * x);
}
inline unsigned char to_u8(float x)
{
const float clamped = clamp01(x);
return static_cast<unsigned char>(clamped * 255.0f + 0.5f);
}
inline float to_f01(unsigned char x)
{
return static_cast<float>(x) / 255.0f;
}
} // namespace
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b)
{
::filament_mixer::lerp(r1, g1, b1, r2, g2, b2, t, out_r, out_g, out_b);
}
void filament_mixer_lerp_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b)
{
unsigned char ur = 0, ug = 0, ub = 0;
filament_mixer_lerp(to_u8(r1), to_u8(g1), to_u8(b1),
to_u8(r2), to_u8(g2), to_u8(b2),
t, &ur, &ug, &ub);
*out_r = to_f01(ur);
*out_g = to_f01(ug);
*out_b = to_f01(ub);
}
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b)
{
const float sr1 = linear_to_srgb(clamp01(r1));
const float sg1 = linear_to_srgb(clamp01(g1));
const float sb1 = linear_to_srgb(clamp01(b1));
const float sr2 = linear_to_srgb(clamp01(r2));
const float sg2 = linear_to_srgb(clamp01(g2));
const float sb2 = linear_to_srgb(clamp01(b2));
float out_sr = 0.0f, out_sg = 0.0f, out_sb = 0.0f;
filament_mixer_lerp_float(sr1, sg1, sb1, sr2, sg2, sb2, t, &out_sr, &out_sg, &out_sb);
*out_r = srgb_to_linear(clamp01(out_sr));
*out_g = srgb_to_linear(clamp01(out_sg));
*out_b = srgb_to_linear(clamp01(out_sb));
}
static bool parse_hex(const std::string &hex, unsigned char &r, unsigned char &g, unsigned char &b)
{
if (hex.size() < 7 || hex[0] != '#') return false;
unsigned rv = 0, gv = 0, bv = 0;
if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &rv, &gv, &bv) != 3) return false;
r = (unsigned char)rv; g = (unsigned char)gv; b = (unsigned char)bv;
return true;
}
std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b)
{
unsigned char r1 = 128, g1 = 128, b1 = 128;
unsigned char r2 = 128, g2 = 128, b2 = 128;
parse_hex(hex_a, r1, g1, b1);
parse_hex(hex_b, r2, g2, b2);
unsigned char mr = 0, mg = 0, mb = 0;
filament_mixer_lerp(r1, g1, b1, r2, g2, b2, ratio_b, &mr, &mg, &mb);
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", mr, mg, mb);
return std::string(buf);
}
std::string blend_color_multi(const std::vector<std::string> &hex_colors,
const std::vector<int> &weights)
{
if (hex_colors.size() >= 2 && hex_colors.size() == weights.size()) {
std::string measured = lookup_measured_blend_color(hex_colors, weights);
if (!measured.empty())
return measured;
}
if (hex_colors.empty())
return "#000000";
if (hex_colors.size() == 1) {
unsigned char cr = 128, cg = 128, cb = 128;
parse_hex(hex_colors.front(), cr, cg, cb);
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", cr, cg, cb);
return std::string(buf);
}
assert(hex_colors.size() == weights.size());
unsigned char r = 128, g = 128, b = 128;
int accumulated = 0;
for (size_t i = 0; i < hex_colors.size() && i < weights.size(); ++i) {
if (weights[i] <= 0)
continue;
unsigned char cr = 128, cg = 128, cb = 128;
parse_hex(hex_colors[i], cr, cg, cb);
if (accumulated == 0) {
r = cr; g = cg; b = cb;
accumulated = weights[i];
} else {
const int new_total = accumulated + weights[i];
const float t = static_cast<float>(weights[i]) / static_cast<float>(new_total);
filament_mixer_lerp(r, g, b, cr, cg, cb, t, &r, &g, &b);
accumulated = new_total;
}
}
if (accumulated == 0)
return "#000000";
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b);
return std::string(buf);
}
std::vector<unsigned int> parse_mixed_components(const std::string &str)
{
std::vector<unsigned int> components;
if (str.empty())
return components;
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
try {
int val = std::stoi(token);
if (val >= 0)
components.push_back(static_cast<unsigned int>(val));
} catch (...) {}
}
return components;
}
namespace {
// Parse a token that may represent a finite double or "use default" (empty / "nan").
// Returns NaN on either explicit sentinel or any parse error.
inline double parse_tangent_token(const std::string& tok)
{
if (tok.empty()) return std::numeric_limits<double>::quiet_NaN();
std::string lower(tok.size(), '\0');
std::transform(tok.begin(), tok.end(), lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lower == "nan") return std::numeric_limits<double>::quiet_NaN();
try {
const double v = std::stod(tok);
if (!std::isfinite(v)) return std::numeric_limits<double>::quiet_NaN();
return v;
} catch (...) {
return std::numeric_limits<double>::quiet_NaN();
}
}
// Split a "a,b,c,d" segment on commas, preserving empty tokens (so "0.5,0.4,," yields
// {"0.5","0.4","",""}). Used by the gradient-curve parser to distinguish NaN tangents
// from a malformed segment.
inline std::vector<std::string> split_commas(const std::string& seg)
{
std::vector<std::string> out;
size_t start = 0;
while (true) {
const size_t comma = seg.find(',', start);
if (comma == std::string::npos) {
out.emplace_back(seg.substr(start));
return out;
}
out.emplace_back(seg.substr(start, comma - start));
start = comma + 1;
}
}
} // namespace
// Default Fritsch-Carlson PCHIP tangents for a sorted-by-x anchor list. m has size n
// matching the anchor count; for n == 1 the tangent is 0; for n == 2 both endpoint
// tangents equal the single secant (degenerates to linear).
std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts)
{
const size_t n = pts.size();
std::vector<double> m(n, 0.0);
if (n < 2) return m;
std::vector<double> d(n - 1);
for (size_t i = 0; i + 1 < n; ++i) {
const double h = std::max(1e-12, pts[i + 1].x - pts[i].x);
d[i] = (pts[i + 1].y - pts[i].y) / h;
}
m[0] = d[0];
m[n - 1] = d[n - 2];
for (size_t i = 1; i + 1 < n; ++i)
m[i] = 0.5 * (d[i - 1] + d[i]);
// Fritsch-Carlson monotonic guard: kill flats then rescale steep tangents so the
// resulting cubic never overshoots [min, max] of the surrounding anchors.
for (size_t i = 0; i + 1 < n; ++i) {
if (d[i] == 0.0) {
m[i] = 0.0;
m[i + 1] = 0.0;
continue;
}
const double a = m[i] / d[i];
const double b = m[i + 1] / d[i];
const double s = a * a + b * b;
if (s > 9.0) {
const double tau = 3.0 / std::sqrt(s);
m[i] = tau * a * d[i];
m[i + 1] = tau * b * d[i];
}
}
return m;
}
GradientCurve parse_gradient_curve(const std::string& s)
{
GradientCurve curve;
if (s.empty())
return curve;
CNumericLocalesSetter c_locale_setter;
std::istringstream ss(s);
std::string segment;
while (std::getline(ss, segment, '|')) {
if (segment.empty())
continue;
const auto fields = split_commas(segment);
// 2-field legacy form -> (x, y), tangents stay NaN.
// 4-field form -> (x, y, m_in, m_out), empty / "nan" tokens preserved as NaN.
if (fields.size() != 2 && fields.size() != 4) {
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring malformed segment \""
<< segment << "\" (expected 2 or 4 comma-separated fields, got "
<< fields.size() << ")";
continue;
}
try {
double x = std::stod(fields[0]);
double y = std::stod(fields[1]);
x = std::max(0.0, std::min(1.0, x));
y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, y));
GradientAnchor a;
a.x = x;
a.y = y;
if (fields.size() == 4) {
a.m_in = parse_tangent_token(fields[2]);
a.m_out = parse_tangent_token(fields[3]);
}
curve.points.push_back(a);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring unparseable segment \""
<< segment << "\": " << e.what();
}
}
if (curve.points.size() < 2) {
if (!curve.points.empty())
BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: only "
<< curve.points.size() << " valid point(s), need at least 2; discarding";
curve.points.clear();
return curve;
}
std::sort(curve.points.begin(), curve.points.end(),
[](const GradientAnchor& a, const GradientAnchor& b) {
return a.x < b.x;
});
return curve;
}
std::string serialize_gradient_curve(const GradientCurve& c)
{
if (c.points.empty())
return std::string{};
CNumericLocalesSetter c_locale_setter;
std::string out;
char buf[128];
for (size_t i = 0; i < c.points.size(); ++i) {
if (i > 0) out += '|';
const auto& a = c.points[i];
const bool has_in = std::isfinite(a.m_in);
const bool has_out = std::isfinite(a.m_out);
if (has_in || has_out) {
// Emit empty tokens for NaN slots so the legacy parser would still split
// four fields; the new parser interprets empty tokens as "use PCHIP default".
char in_buf[32] = {0};
char out_buf[32] = {0};
if (has_in) std::snprintf(in_buf, sizeof(in_buf), "%.4f", a.m_in);
if (has_out) std::snprintf(out_buf, sizeof(out_buf), "%.4f", a.m_out);
std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%s,%s",
a.x, a.y, in_buf, out_buf);
} else {
// 4-field form is only emitted when at least one tangent is finite; the
// 2-field form is emitted otherwise so the JSON payload stays minimal
// and remains readable by older clients that only know (x, y) pairs.
std::snprintf(buf, sizeof(buf), "%.4f,%.4f", a.x, a.y);
}
out += buf;
}
return out;
}
double sample_gradient_curve(const GradientCurve& c, double t)
{
const auto& pts = c.points;
if (pts.size() < 2)
return 0.5;
if (t <= pts.front().x)
return pts.front().y;
if (t >= pts.back().x)
return pts.back().y;
// PCHIP defaults are computed for every call; control point counts are typically
// tiny (< 16) so the allocation cost is negligible compared to any actual rendering
// or G-code work that drives the sampler.
const std::vector<double> m_def = compute_pchip_default_tangents(pts);
const size_t n = pts.size();
// Linear scan to locate the interval [pts[i].x, pts[i+1].x] containing t. Cheap
// and avoids the upper_bound boilerplate; n is small.
for (size_t i = 1; i < n; ++i) {
const double x0 = pts[i - 1].x;
const double x1 = pts[i].x;
if (t > x1) continue;
const double y0 = pts[i - 1].y;
const double y1 = pts[i].y;
const double h = std::max(1e-12, x1 - x0);
const double m_left = std::isfinite(pts[i - 1].m_out) ? pts[i - 1].m_out : m_def[i - 1];
const double m_right = std::isfinite(pts[i].m_in) ? pts[i].m_in : m_def[i];
const double u = (t - x0) / h;
const double u2 = u * u;
const double u3 = u2 * u;
const double h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
const double h10 = u3 - 2.0 * u2 + u;
const double h01 = -2.0 * u3 + 3.0 * u2;
const double h11 = u3 - u2;
double y = h00 * y0 + h10 * h * m_left
+ h01 * y1 + h11 * h * m_right;
// Defensive clamp in case tangent overrides on legacy curves push the
// single-segment Hermite slightly outside the anchor band.
if (y < kGradientMinRatio) y = kGradientMinRatio;
if (y > kGradientMaxRatio) y = kGradientMaxRatio;
return y;
}
return pts.back().y;
}
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components)
{
CNumericLocalesSetter c_locale_setter;
std::vector<double> ratios;
if (!str.empty()) {
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
try {
double val = std::stod(token);
if (val > 0.0)
ratios.push_back(val);
} catch (...) {}
}
}
if (ratios.size() != n_components || n_components == 0) {
ratios.assign(n_components, n_components > 0 ? 1.0 / n_components : 0.0);
return ratios;
}
double sum = std::accumulate(ratios.begin(), ratios.end(), 0.0);
if (sum > 0.0 && std::abs(sum - 1.0) > 1e-6) {
for (double &r : ratios)
r /= sum;
}
return ratios;
}
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
{
for (unsigned char v : is_mixed)
if (v) return true;
return false;
}
std::vector<size_t> check_mixed_filament_integrity(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
size_t num_physical)
{
std::vector<size_t> broken;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i]) continue;
if (i >= comp_strs.size() || comp_strs[i].empty()) {
broken.push_back(i);
continue;
}
auto comps = parse_mixed_components(comp_strs[i]);
if (comps.size() < 2) {
broken.push_back(i);
continue;
}
for (unsigned int c : comps) {
if (c < 1 || c > num_physical) {
broken.push_back(i);
break;
}
}
}
return broken;
}
std::vector<unsigned int> expand_mixed_filaments(
const std::vector<unsigned int> &extruders_0based,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs)
{
std::vector<unsigned int> result;
for (unsigned int ext : extruders_0based) {
if (ext < is_mixed.size() && is_mixed[ext] && ext < comp_strs.size()) {
auto comps = parse_mixed_components(comp_strs[ext]);
for (unsigned int c : comps)
if (c >= 1) result.push_back(c - 1);
} else {
result.push_back(ext);
}
}
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
void remap_mixed_components_on_delete(
const std::vector<unsigned char> &is_mixed,
std::vector<std::string> &comp_strs,
unsigned int del_1based)
{
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i]) continue;
if (i >= comp_strs.size() || comp_strs[i].empty()) continue;
auto comps = parse_mixed_components(comp_strs[i]);
std::ostringstream ss;
for (size_t j = 0; j < comps.size(); ++j) {
if (j > 0) ss << ',';
if (comps[j] == del_1based)
ss << 0;
else if (comps[j] > del_1based)
ss << (comps[j] - 1);
else
ss << comps[j];
}
comp_strs[i] = ss.str();
}
}
std::vector<size_t> check_mixed_filament_type_consistency(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &filament_types)
{
std::vector<size_t> result;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i]) continue;
if (i >= comp_strs.size() || comp_strs[i].empty()) continue;
auto comps = parse_mixed_components(comp_strs[i]);
if (comps.size() < 2) continue;
std::string ref_type;
bool mismatch = false;
for (unsigned int c : comps) {
if (c == 0) continue; // sentinel for deleted component
size_t idx = static_cast<size_t>(c) - 1; // 1-based -> 0-based
if (idx >= filament_types.size()) continue;
if (ref_type.empty())
ref_type = filament_types[idx];
else if (filament_types[idx] != ref_type) {
mismatch = true;
break;
}
}
if (mismatch)
result.push_back(i);
}
return result;
}
void expand_mixed_slots_in_unprintables(
std::vector<std::set<int>> &unprintables,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs)
{
for (auto &unprintable_set : unprintables) {
std::set<int> expanded;
for (int fid : unprintable_set) {
if (fid >= 0 && (size_t)fid < is_mixed.size() && is_mixed[fid]
&& (size_t)fid < comp_strs.size()) {
auto comps = parse_mixed_components(comp_strs[fid]);
for (unsigned int c : comps)
if (c >= 1) expanded.insert((int)(c - 1));
} else {
expanded.insert(fid);
}
}
unprintable_set = std::move(expanded);
}
}
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals)
{
for (size_t i = 0; i < vals.size(); ++i) {
if (vals[i].empty())
continue;
// parse_gradient_curve returns empty for both "empty input" and "<2 valid points";
// we already skipped empty, so an empty result means a corrupted single-point slot.
if (parse_gradient_curve(vals[i]).empty()) {
BOOST_LOG_TRIVIAL(warning) << "sanitize_mixed_gradient_curve_array: slot "
<< i << " curve \"" << vals[i]
<< "\" has fewer than 2 valid points; clearing to linear";
vals[i].clear();
}
}
}
bool try_parse_mixed_components_strict(const std::string &str,
std::vector<unsigned int> &components,
std::string &err)
{
components.clear();
if (str.empty()) {
err = "empty component list";
return false;
}
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty component index";
return false;
}
try {
const long val = std::stol(token);
if (val < 1) {
err = "component index must be >= 1 (got " + token + ")";
return false;
}
components.push_back(static_cast<unsigned int>(val));
} catch (...) {
err = "invalid component index \"" + token + "\"";
return false;
}
}
if (components.size() < 2) {
err = "at least 2 components required (got " + std::to_string(components.size()) + ")";
return false;
}
std::set<unsigned int> seen;
for (unsigned int c : components) {
if (!seen.insert(c).second) {
err = "duplicate component index " + std::to_string(c);
return false;
}
}
return true;
}
bool try_parse_mixed_ratios_strict(const std::string &str,
size_t n_components,
std::string &err)
{
if (str.empty())
return true;
CNumericLocalesSetter c_locale_setter;
std::vector<double> ratios;
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty ratio value";
return false;
}
try {
const double val = std::stod(token);
if (!(val > 0.0)) {
err = "ratio must be positive (got " + token + ")";
return false;
}
ratios.push_back(val);
} catch (...) {
err = "invalid ratio \"" + token + "\"";
return false;
}
}
if (ratios.size() != n_components) {
err = "expected " + std::to_string(n_components) + " ratio(s), got "
+ std::to_string(ratios.size());
return false;
}
return true;
}
bool validate_gradient_range_strict(const std::string &str, std::string &err)
{
if (str.empty())
return true;
CNumericLocalesSetter c_locale_setter;
float v0 = 0.f, v1 = 0.f;
if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) {
err = "expected two comma-separated floats, e.g. \"0.10,0.90\"";
return false;
}
if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) {
err = "start and end ratios must be in (0, 1)";
return false;
}
return true;
}
static void append_error(std::map<std::string, std::string> &errors,
const std::string &key,
const std::string &msg)
{
auto it = errors.find(key);
if (it == errors.end())
errors.emplace(key, msg);
else
it->second += "; " + msg;
}
static bool has_mixed_sub_params_specified(
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags)
{
for (const std::string &s : comp_strs)
if (!s.empty()) return true;
for (const std::string &s : ratio_strs)
if (!s.empty()) return true;
for (unsigned char g : gradient_flags)
if (g) return true;
return false;
}
static bool mixed_string_array_was_specified(const std::vector<std::string> &vals)
{
for (const std::string &s : vals)
if (!s.empty())
return true;
return false;
}
static bool mixed_bool_array_was_specified(const std::vector<unsigned char> &vals)
{
for (unsigned char v : vals)
if (v)
return true;
return false;
}
static void check_mixed_array_size_required(std::map<std::string, std::string> &errors,
const std::string &opt_key,
size_t actual_size,
size_t expected_size)
{
if (actual_size != expected_size) {
append_error(errors, opt_key,
"array size " + std::to_string(actual_size)
+ " does not match filament slot count " + std::to_string(expected_size));
}
}
std::map<std::string, std::string> validate_mixed_filament_params(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags,
const std::vector<std::string> &gradient_range_strs,
const std::vector<std::string> &gradient_curve_strs)
{
std::map<std::string, std::string> errors;
if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags)
&& !has_any_mixed_filament(is_mixed)) {
append_error(errors, "filament_is_mixed",
"must be set when mixed filament parameters are specified");
return errors;
}
if (!has_any_mixed_filament(is_mixed))
return errors;
const size_t slot_count = is_mixed.size();
// Rule 1: mixed filament model → components & ratios arrays must cover every slot.
check_mixed_array_size_required(errors, "filament_mixed_components", comp_strs.size(), slot_count);
check_mixed_array_size_required(errors, "filament_mixed_sublayer_ratios", ratio_strs.size(), slot_count);
// Rule 2: gradient passed (any slot true) → gradient & range arrays must cover every slot.
const bool gradient_specified = mixed_bool_array_was_specified(gradient_flags);
if (gradient_specified) {
check_mixed_array_size_required(errors, "filament_mixed_gradient", gradient_flags.size(), slot_count);
check_mixed_array_size_required(errors, "filament_mixed_gradient_range", gradient_range_strs.size(), slot_count);
}
// Rule 3: curve passed (any non-empty entry) → curve array must cover every slot.
const bool curve_specified = mixed_string_array_was_specified(gradient_curve_strs);
if (curve_specified)
check_mixed_array_size_required(errors, "filament_mixed_gradient_curve", gradient_curve_strs.size(), slot_count);
size_t num_physical = 0;
for (unsigned char v : is_mixed)
if (!v) ++num_physical;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
const std::string slot = "slot " + std::to_string(i + 1);
const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : "";
std::vector<unsigned int> components;
std::string comp_err;
if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) {
append_error(errors, "filament_mixed_components", slot + ": " + comp_err);
continue;
}
for (unsigned int c : components) {
if (c > num_physical) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " out of range (max physical filament index is "
+ std::to_string(num_physical) + ")");
break;
}
if (c == i + 1) {
append_error(errors, "filament_mixed_components",
slot + ": cannot reference itself as a component");
break;
}
const size_t idx0 = static_cast<size_t>(c - 1);
if (idx0 < is_mixed.size() && is_mixed[idx0]) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " references a mixed filament slot");
break;
}
}
std::string ratio_err;
const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : "";
if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err))
append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err);
const bool gradient_on = i < gradient_flags.size() && gradient_flags[i];
if (gradient_on) {
if (components.size() != 2) {
append_error(errors, "filament_mixed_gradient",
slot + ": gradient requires exactly 2 components");
}
if (gradient_specified) {
std::string range_err;
const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : "";
if (!validate_gradient_range_strict(range_str, range_err))
append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err);
}
if (curve_specified) {
const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : "";
if (!curve_str.empty() && parse_gradient_curve(curve_str).empty())
append_error(errors, "filament_mixed_gradient_curve",
slot + ": invalid curve (need at least 2 valid control points)");
}
}
}
return errors;
}
} // namespace Slic3r
+164
View File
@@ -0,0 +1,164 @@
#ifndef SLIC3R_FILAMENT_MIXER_HPP
#define SLIC3R_FILAMENT_MIXER_HPP
#include <limits>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
// Photoshop-style gradient curve control point in [0,1] x [0,1].
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
// anchors loaded from old 2-field 3MF projects or freshly added via a quick click.
// A press-and-drag on a curve segment populates m_out of its left anchor and m_in of
// its right anchor so the segment bends without inserting a new anchor.
struct GradientAnchor {
double x = 0.0;
double y = 0.0;
double m_in = std::numeric_limits<double>::quiet_NaN();
double m_out = std::numeric_limits<double>::quiet_NaN();
};
// Sorted list of GradientAnchor; x in [0,1], y in [kGradientMinRatio, kGradientMaxRatio].
// Empty means "no custom curve" (callers should fall back to the linear range).
struct GradientCurve {
std::vector<GradientAnchor> points;
bool empty() const { return points.empty(); }
};
// Reserved blend ratio range. Anchor y values (= component 0's ratio) are constrained
// to this band so the mixed filament never reaches pure 0% / 100% of either physical
// component, which keeps both extruders flowing and avoids degenerate transitions.
// Both the editor and the sampler enforce this clamp.
constexpr double kGradientMinRatio = 0.1;
constexpr double kGradientMaxRatio = 0.9;
// Parse "x0,y0[,m_in0,m_out0]|x1,y1[,m_in1,m_out1]|..." into a GradientCurve.
// (Anchors are pipe-separated; the fields within an anchor are comma-separated.)
// Accepts both the legacy 2-field form (tangents -> NaN) and the new 4-field form
// (empty token or "nan" preserved as NaN). Returns an empty curve when the input is
// empty or unparsable. Points are clamped to [0,1] for (x, y) and re-sorted by x.
GradientCurve parse_gradient_curve(const std::string& s);
// Serialize a GradientCurve back to a string. Emits 4 fields per anchor when any
// tangent override is finite; emits 2 fields when both tangents are NaN so unchanged
// projects stay byte-identical with the legacy format. Returns "" when empty.
std::string serialize_gradient_curve(const GradientCurve& c);
// Sample the curve at t in [0,1] using cubic Hermite with Fritsch-Carlson PCHIP
// default tangents, optionally overridden per anchor via m_in / m_out. Returns the
// clamped end values when t is outside the control point range. Returns 0.5 when the
// curve has fewer than 2 points (a safety fallback; callers should check empty()).
double sample_gradient_curve(const GradientCurve& c, double t);
// Compute Fritsch-Carlson PCHIP default tangents for a sorted-by-x anchor list.
// Result size == pts.size(). Useful for callers that need to know what tangent the
// sampler would synthesize when m_in / m_out are NaN (e.g. the GUI's segment-bend
// interaction that inserts a virtual anchor and reads back the surrounding tangents).
std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts);
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b);
void filament_mixer_lerp_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b);
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b);
// Blend two hex colors ("#RRGGBB") by ratio (0.0 ~ 1.0 for color_b).
// Returns "#RRGGBB" string.
std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b);
// Blend N hex colors by integer weights using polynomial pigment mixing.
// Pairwise accumulation via filament_mixer_lerp. Returns "#RRGGBB".
std::string blend_color_multi(const std::vector<std::string> &hex_colors,
const std::vector<int> &weights);
// Parse comma-separated 1-based component IDs, e.g. "1,3" → {1, 3}.
std::vector<unsigned int> parse_mixed_components(const std::string &str);
// Parse comma-separated ratio values, e.g. "0.7,0.3" → {0.7, 0.3}.
// Returns equal ratios (1/n each) when str is empty or invalid.
// Normalizes so the sum equals 1.0.
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
// Returns true if any element in is_mixed is true.
// ConfigOptionBools stores values as std::vector<unsigned char>.
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
// Check which mixed filament slots have broken component references.
// Returns 0-based indices of mixed slots whose components reference
// filaments beyond num_physical (i.e., deleted filaments).
std::vector<size_t> check_mixed_filament_integrity(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
size_t num_physical);
// Expand mixed filament slots in an extruder list to their physical components.
// Input/output are 0-based indices. Non-mixed slots pass through unchanged.
// Result is sorted and deduplicated.
std::vector<unsigned int> expand_mixed_filaments(
const std::vector<unsigned int> &extruders_0based,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs);
// Remap mixed filament component references after a physical filament is deleted.
// del_1based: the 1-based index of the deleted physical filament.
// For each mixed slot:
// - if component == del_1based -> replace with 0 (sentinel for deleted/unselected)
// - if component > del_1based -> decrement by 1
void remap_mixed_components_on_delete(
const std::vector<unsigned char> &is_mixed,
std::vector<std::string> &comp_strs,
unsigned int del_1based);
// Check which mixed filament slots have type-mismatched components.
// filament_types: type strings for physical filaments (0-based, size == num_physical).
// Component IDs in comp_strs are 1-based; the function converts to 0-based to look up types.
// Returns 0-based config indices of mixed slots with mismatched component types.
std::vector<size_t> check_mixed_filament_type_consistency(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &filament_types);
// Expand mixed-slot IDs in geometric unprintable sets to their physical component IDs.
// Each set entry that corresponds to a mixed slot is replaced by the slot's component
// IDs (0-based). Non-mixed entries pass through unchanged.
void expand_mixed_slots_in_unprintables(
std::vector<std::set<int>> &unprintables,
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs);
// Clear any non-empty gradient-curve slot that parses to fewer than 2 control points.
// Heals per-slot arrays corrupted by the legacy "|" separator collision between
// PresetBundle::export_selections / load_selections (which used "|" as the inter-slot
// delimiter) and serialize_gradient_curve / parse_gradient_curve (which use "|" as the
// intra-slot control-point delimiter). Such a round-trip splits a multi-point curve
// across adjacent slots, leaving single-point entries that fail MakerWorld's strict
// "curve needs >= 2 points" check. Clearing them falls back to the linear range.
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals);
// Validate mixed-color (混色) parameters. Returns error messages keyed by option name.
// Slot details are included in the message text (1-based slot index).
std::map<std::string, std::string> validate_mixed_filament_params(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags,
const std::vector<std::string> &gradient_range_strs,
const std::vector<std::string> &gradient_curve_strs);
} // namespace Slic3r
#endif // SLIC3R_FILAMENT_MIXER_HPP
+819
View File
@@ -0,0 +1,819 @@
/*
* FilamentMixer — Header-only C++ pigment color mixer
*
* Filament mixer implementation using a degree-4 polynomial regression
* trained to approximate Mixbox behavior (Mean Delta-E ~2.07).
* This library does not include Mixbox source code, binaries, or data files.
*
* Usage:
* #include "FilamentMixerModel.hpp"
*
* unsigned char r, g, b;
* filament_mixer::lerp(0, 33, 133, 252, 211, 0, 0.5f, &r, &g, &b);
* // r=47, g=141, b=56 (blue + yellow → green)
*
* No dependencies beyond the C++ standard library.
*
* MIT License
*
* Copyright (c) 2026 Justin Hayes
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef FILAMENT_MIXER_MODEL_HPP
#define FILAMENT_MIXER_MODEL_HPP
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace filament_mixer {
namespace detail {
// BEGIN AUTO-GENERATED COEFFICIENTS
// Auto-generated by scripts/export_poly_coefficients.py
// Do not edit manually.
// Degree-4 polynomial, 330 features, 7 inputs
static const int POLY_DEGREE = 4;
static const int N_FEATURES = 330;
static const int N_INPUTS = 7;
static const int POWERS[330][7] = {
{0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 1},
{2, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0},
{1, 0, 1, 0, 0, 0, 0},
{1, 0, 0, 1, 0, 0, 0},
{1, 0, 0, 0, 1, 0, 0},
{1, 0, 0, 0, 0, 1, 0},
{1, 0, 0, 0, 0, 0, 1},
{0, 2, 0, 0, 0, 0, 0},
{0, 1, 1, 0, 0, 0, 0},
{0, 1, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 1, 0, 0},
{0, 1, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1},
{0, 0, 2, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 0, 1, 0, 0},
{0, 0, 1, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 1},
{0, 0, 0, 2, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0},
{0, 0, 0, 1, 0, 1, 0},
{0, 0, 0, 1, 0, 0, 1},
{0, 0, 0, 0, 2, 0, 0},
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 1, 0, 1},
{0, 0, 0, 0, 0, 2, 0},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 2},
{3, 0, 0, 0, 0, 0, 0},
{2, 1, 0, 0, 0, 0, 0},
{2, 0, 1, 0, 0, 0, 0},
{2, 0, 0, 1, 0, 0, 0},
{2, 0, 0, 0, 1, 0, 0},
{2, 0, 0, 0, 0, 1, 0},
{2, 0, 0, 0, 0, 0, 1},
{1, 2, 0, 0, 0, 0, 0},
{1, 1, 1, 0, 0, 0, 0},
{1, 1, 0, 1, 0, 0, 0},
{1, 1, 0, 0, 1, 0, 0},
{1, 1, 0, 0, 0, 1, 0},
{1, 1, 0, 0, 0, 0, 1},
{1, 0, 2, 0, 0, 0, 0},
{1, 0, 1, 1, 0, 0, 0},
{1, 0, 1, 0, 1, 0, 0},
{1, 0, 1, 0, 0, 1, 0},
{1, 0, 1, 0, 0, 0, 1},
{1, 0, 0, 2, 0, 0, 0},
{1, 0, 0, 1, 1, 0, 0},
{1, 0, 0, 1, 0, 1, 0},
{1, 0, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 2, 0, 0},
{1, 0, 0, 0, 1, 1, 0},
{1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 0, 2, 0},
{1, 0, 0, 0, 0, 1, 1},
{1, 0, 0, 0, 0, 0, 2},
{0, 3, 0, 0, 0, 0, 0},
{0, 2, 1, 0, 0, 0, 0},
{0, 2, 0, 1, 0, 0, 0},
{0, 2, 0, 0, 1, 0, 0},
{0, 2, 0, 0, 0, 1, 0},
{0, 2, 0, 0, 0, 0, 1},
{0, 1, 2, 0, 0, 0, 0},
{0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 1, 0, 0},
{0, 1, 1, 0, 0, 1, 0},
{0, 1, 1, 0, 0, 0, 1},
{0, 1, 0, 2, 0, 0, 0},
{0, 1, 0, 1, 1, 0, 0},
{0, 1, 0, 1, 0, 1, 0},
{0, 1, 0, 1, 0, 0, 1},
{0, 1, 0, 0, 2, 0, 0},
{0, 1, 0, 0, 1, 1, 0},
{0, 1, 0, 0, 1, 0, 1},
{0, 1, 0, 0, 0, 2, 0},
{0, 1, 0, 0, 0, 1, 1},
{0, 1, 0, 0, 0, 0, 2},
{0, 0, 3, 0, 0, 0, 0},
{0, 0, 2, 1, 0, 0, 0},
{0, 0, 2, 0, 1, 0, 0},
{0, 0, 2, 0, 0, 1, 0},
{0, 0, 2, 0, 0, 0, 1},
{0, 0, 1, 2, 0, 0, 0},
{0, 0, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 0, 1, 0},
{0, 0, 1, 1, 0, 0, 1},
{0, 0, 1, 0, 2, 0, 0},
{0, 0, 1, 0, 1, 1, 0},
{0, 0, 1, 0, 1, 0, 1},
{0, 0, 1, 0, 0, 2, 0},
{0, 0, 1, 0, 0, 1, 1},
{0, 0, 1, 0, 0, 0, 2},
{0, 0, 0, 3, 0, 0, 0},
{0, 0, 0, 2, 1, 0, 0},
{0, 0, 0, 2, 0, 1, 0},
{0, 0, 0, 2, 0, 0, 1},
{0, 0, 0, 1, 2, 0, 0},
{0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 1, 1, 0, 1},
{0, 0, 0, 1, 0, 2, 0},
{0, 0, 0, 1, 0, 1, 1},
{0, 0, 0, 1, 0, 0, 2},
{0, 0, 0, 0, 3, 0, 0},
{0, 0, 0, 0, 2, 1, 0},
{0, 0, 0, 0, 2, 0, 1},
{0, 0, 0, 0, 1, 2, 0},
{0, 0, 0, 0, 1, 1, 1},
{0, 0, 0, 0, 1, 0, 2},
{0, 0, 0, 0, 0, 3, 0},
{0, 0, 0, 0, 0, 2, 1},
{0, 0, 0, 0, 0, 1, 2},
{0, 0, 0, 0, 0, 0, 3},
{4, 0, 0, 0, 0, 0, 0},
{3, 1, 0, 0, 0, 0, 0},
{3, 0, 1, 0, 0, 0, 0},
{3, 0, 0, 1, 0, 0, 0},
{3, 0, 0, 0, 1, 0, 0},
{3, 0, 0, 0, 0, 1, 0},
{3, 0, 0, 0, 0, 0, 1},
{2, 2, 0, 0, 0, 0, 0},
{2, 1, 1, 0, 0, 0, 0},
{2, 1, 0, 1, 0, 0, 0},
{2, 1, 0, 0, 1, 0, 0},
{2, 1, 0, 0, 0, 1, 0},
{2, 1, 0, 0, 0, 0, 1},
{2, 0, 2, 0, 0, 0, 0},
{2, 0, 1, 1, 0, 0, 0},
{2, 0, 1, 0, 1, 0, 0},
{2, 0, 1, 0, 0, 1, 0},
{2, 0, 1, 0, 0, 0, 1},
{2, 0, 0, 2, 0, 0, 0},
{2, 0, 0, 1, 1, 0, 0},
{2, 0, 0, 1, 0, 1, 0},
{2, 0, 0, 1, 0, 0, 1},
{2, 0, 0, 0, 2, 0, 0},
{2, 0, 0, 0, 1, 1, 0},
{2, 0, 0, 0, 1, 0, 1},
{2, 0, 0, 0, 0, 2, 0},
{2, 0, 0, 0, 0, 1, 1},
{2, 0, 0, 0, 0, 0, 2},
{1, 3, 0, 0, 0, 0, 0},
{1, 2, 1, 0, 0, 0, 0},
{1, 2, 0, 1, 0, 0, 0},
{1, 2, 0, 0, 1, 0, 0},
{1, 2, 0, 0, 0, 1, 0},
{1, 2, 0, 0, 0, 0, 1},
{1, 1, 2, 0, 0, 0, 0},
{1, 1, 1, 1, 0, 0, 0},
{1, 1, 1, 0, 1, 0, 0},
{1, 1, 1, 0, 0, 1, 0},
{1, 1, 1, 0, 0, 0, 1},
{1, 1, 0, 2, 0, 0, 0},
{1, 1, 0, 1, 1, 0, 0},
{1, 1, 0, 1, 0, 1, 0},
{1, 1, 0, 1, 0, 0, 1},
{1, 1, 0, 0, 2, 0, 0},
{1, 1, 0, 0, 1, 1, 0},
{1, 1, 0, 0, 1, 0, 1},
{1, 1, 0, 0, 0, 2, 0},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 2},
{1, 0, 3, 0, 0, 0, 0},
{1, 0, 2, 1, 0, 0, 0},
{1, 0, 2, 0, 1, 0, 0},
{1, 0, 2, 0, 0, 1, 0},
{1, 0, 2, 0, 0, 0, 1},
{1, 0, 1, 2, 0, 0, 0},
{1, 0, 1, 1, 1, 0, 0},
{1, 0, 1, 1, 0, 1, 0},
{1, 0, 1, 1, 0, 0, 1},
{1, 0, 1, 0, 2, 0, 0},
{1, 0, 1, 0, 1, 1, 0},
{1, 0, 1, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 2, 0},
{1, 0, 1, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 0, 2},
{1, 0, 0, 3, 0, 0, 0},
{1, 0, 0, 2, 1, 0, 0},
{1, 0, 0, 2, 0, 1, 0},
{1, 0, 0, 2, 0, 0, 1},
{1, 0, 0, 1, 2, 0, 0},
{1, 0, 0, 1, 1, 1, 0},
{1, 0, 0, 1, 1, 0, 1},
{1, 0, 0, 1, 0, 2, 0},
{1, 0, 0, 1, 0, 1, 1},
{1, 0, 0, 1, 0, 0, 2},
{1, 0, 0, 0, 3, 0, 0},
{1, 0, 0, 0, 2, 1, 0},
{1, 0, 0, 0, 2, 0, 1},
{1, 0, 0, 0, 1, 2, 0},
{1, 0, 0, 0, 1, 1, 1},
{1, 0, 0, 0, 1, 0, 2},
{1, 0, 0, 0, 0, 3, 0},
{1, 0, 0, 0, 0, 2, 1},
{1, 0, 0, 0, 0, 1, 2},
{1, 0, 0, 0, 0, 0, 3},
{0, 4, 0, 0, 0, 0, 0},
{0, 3, 1, 0, 0, 0, 0},
{0, 3, 0, 1, 0, 0, 0},
{0, 3, 0, 0, 1, 0, 0},
{0, 3, 0, 0, 0, 1, 0},
{0, 3, 0, 0, 0, 0, 1},
{0, 2, 2, 0, 0, 0, 0},
{0, 2, 1, 1, 0, 0, 0},
{0, 2, 1, 0, 1, 0, 0},
{0, 2, 1, 0, 0, 1, 0},
{0, 2, 1, 0, 0, 0, 1},
{0, 2, 0, 2, 0, 0, 0},
{0, 2, 0, 1, 1, 0, 0},
{0, 2, 0, 1, 0, 1, 0},
{0, 2, 0, 1, 0, 0, 1},
{0, 2, 0, 0, 2, 0, 0},
{0, 2, 0, 0, 1, 1, 0},
{0, 2, 0, 0, 1, 0, 1},
{0, 2, 0, 0, 0, 2, 0},
{0, 2, 0, 0, 0, 1, 1},
{0, 2, 0, 0, 0, 0, 2},
{0, 1, 3, 0, 0, 0, 0},
{0, 1, 2, 1, 0, 0, 0},
{0, 1, 2, 0, 1, 0, 0},
{0, 1, 2, 0, 0, 1, 0},
{0, 1, 2, 0, 0, 0, 1},
{0, 1, 1, 2, 0, 0, 0},
{0, 1, 1, 1, 1, 0, 0},
{0, 1, 1, 1, 0, 1, 0},
{0, 1, 1, 1, 0, 0, 1},
{0, 1, 1, 0, 2, 0, 0},
{0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 0, 1},
{0, 1, 1, 0, 0, 2, 0},
{0, 1, 1, 0, 0, 1, 1},
{0, 1, 1, 0, 0, 0, 2},
{0, 1, 0, 3, 0, 0, 0},
{0, 1, 0, 2, 1, 0, 0},
{0, 1, 0, 2, 0, 1, 0},
{0, 1, 0, 2, 0, 0, 1},
{0, 1, 0, 1, 2, 0, 0},
{0, 1, 0, 1, 1, 1, 0},
{0, 1, 0, 1, 1, 0, 1},
{0, 1, 0, 1, 0, 2, 0},
{0, 1, 0, 1, 0, 1, 1},
{0, 1, 0, 1, 0, 0, 2},
{0, 1, 0, 0, 3, 0, 0},
{0, 1, 0, 0, 2, 1, 0},
{0, 1, 0, 0, 2, 0, 1},
{0, 1, 0, 0, 1, 2, 0},
{0, 1, 0, 0, 1, 1, 1},
{0, 1, 0, 0, 1, 0, 2},
{0, 1, 0, 0, 0, 3, 0},
{0, 1, 0, 0, 0, 2, 1},
{0, 1, 0, 0, 0, 1, 2},
{0, 1, 0, 0, 0, 0, 3},
{0, 0, 4, 0, 0, 0, 0},
{0, 0, 3, 1, 0, 0, 0},
{0, 0, 3, 0, 1, 0, 0},
{0, 0, 3, 0, 0, 1, 0},
{0, 0, 3, 0, 0, 0, 1},
{0, 0, 2, 2, 0, 0, 0},
{0, 0, 2, 1, 1, 0, 0},
{0, 0, 2, 1, 0, 1, 0},
{0, 0, 2, 1, 0, 0, 1},
{0, 0, 2, 0, 2, 0, 0},
{0, 0, 2, 0, 1, 1, 0},
{0, 0, 2, 0, 1, 0, 1},
{0, 0, 2, 0, 0, 2, 0},
{0, 0, 2, 0, 0, 1, 1},
{0, 0, 2, 0, 0, 0, 2},
{0, 0, 1, 3, 0, 0, 0},
{0, 0, 1, 2, 1, 0, 0},
{0, 0, 1, 2, 0, 1, 0},
{0, 0, 1, 2, 0, 0, 1},
{0, 0, 1, 1, 2, 0, 0},
{0, 0, 1, 1, 1, 1, 0},
{0, 0, 1, 1, 1, 0, 1},
{0, 0, 1, 1, 0, 2, 0},
{0, 0, 1, 1, 0, 1, 1},
{0, 0, 1, 1, 0, 0, 2},
{0, 0, 1, 0, 3, 0, 0},
{0, 0, 1, 0, 2, 1, 0},
{0, 0, 1, 0, 2, 0, 1},
{0, 0, 1, 0, 1, 2, 0},
{0, 0, 1, 0, 1, 1, 1},
{0, 0, 1, 0, 1, 0, 2},
{0, 0, 1, 0, 0, 3, 0},
{0, 0, 1, 0, 0, 2, 1},
{0, 0, 1, 0, 0, 1, 2},
{0, 0, 1, 0, 0, 0, 3},
{0, 0, 0, 4, 0, 0, 0},
{0, 0, 0, 3, 1, 0, 0},
{0, 0, 0, 3, 0, 1, 0},
{0, 0, 0, 3, 0, 0, 1},
{0, 0, 0, 2, 2, 0, 0},
{0, 0, 0, 2, 1, 1, 0},
{0, 0, 0, 2, 1, 0, 1},
{0, 0, 0, 2, 0, 2, 0},
{0, 0, 0, 2, 0, 1, 1},
{0, 0, 0, 2, 0, 0, 2},
{0, 0, 0, 1, 3, 0, 0},
{0, 0, 0, 1, 2, 1, 0},
{0, 0, 0, 1, 2, 0, 1},
{0, 0, 0, 1, 1, 2, 0},
{0, 0, 0, 1, 1, 1, 1},
{0, 0, 0, 1, 1, 0, 2},
{0, 0, 0, 1, 0, 3, 0},
{0, 0, 0, 1, 0, 2, 1},
{0, 0, 0, 1, 0, 1, 2},
{0, 0, 0, 1, 0, 0, 3},
{0, 0, 0, 0, 4, 0, 0},
{0, 0, 0, 0, 3, 1, 0},
{0, 0, 0, 0, 3, 0, 1},
{0, 0, 0, 0, 2, 2, 0},
{0, 0, 0, 0, 2, 1, 1},
{0, 0, 0, 0, 2, 0, 2},
{0, 0, 0, 0, 1, 3, 0},
{0, 0, 0, 0, 1, 2, 1},
{0, 0, 0, 0, 1, 1, 2},
{0, 0, 0, 0, 1, 0, 3},
{0, 0, 0, 0, 0, 4, 0},
{0, 0, 0, 0, 0, 3, 1},
{0, 0, 0, 0, 0, 2, 2},
{0, 0, 0, 0, 0, 1, 3},
{0, 0, 0, 0, 0, 0, 4}
};
static const double COEF[330][3] = {
{8.70954844857314666e-12, 1.27926950848359881e-09, -2.06865474316332923e-09},
{1.05783308354771544e+00, -8.02119209663359686e-03, -7.88705651445470723e-02},
{1.35905954452774837e-02, 8.71267975138422468e-01, 1.04898760410704936e-01},
{-4.16452026099768252e-02, 1.75465381596434100e-02, 1.00224594702931546e+00},
{4.50321316661211821e-02, -7.11409155427628892e-02, 3.91232300778902690e-03},
{1.76675507851922452e-02, -1.32709276116036640e-01, 6.36935270589509828e-02},
{-5.23434830565911030e-02, 3.77681739012521722e-02, -2.08691145087504179e-02},
{-2.33722556520224792e-03, -1.57542611462692145e-03, -3.05158628452478807e-03},
{-8.87678609044812990e-04, 3.83194388837734693e-04, 1.37779212442523083e-03},
{-2.11519042076831979e-03, 5.82337362515735358e-04, 2.24055108941204821e-04},
{4.61545125563611917e-04, 7.72869451707915893e-04, -1.10800630143346882e-03},
{1.05937484157345879e-03, -3.14448681732842211e-04, -1.75129182446198098e-03},
{1.49045689016363055e-03, -2.09220860101674106e-04, 5.93100338908187697e-04},
{-3.51246656293852696e-04, -8.20743017485394289e-04, 5.71854064480802862e-04},
{-9.18204643629581319e-01, -2.27788122702773155e-01, 6.39980793022790623e-02},
{9.24243491377523679e-05, 7.32841332381495400e-04, -1.55219718415109450e-03},
{7.13695056804217989e-04, -8.46467621879685712e-05, 6.50202947442505750e-04},
{1.66640864747485983e-03, -1.24492362771216523e-04, 2.68236502346156410e-04},
{-7.20253644860527516e-04, 7.81434220384157334e-04, 1.12661089007361367e-03},
{-6.83033334365238206e-05, 7.27742627159490762e-04, -1.78048843835204584e-03},
{-3.13431571993316588e-02, -8.57604034845650287e-01, -2.57225920656276863e-01},
{-6.47867200595898341e-05, -1.16688982572457655e-03, 1.14174511750260031e-03},
{-5.00713925613324338e-04, -6.87598082111323477e-04, 6.20598069880440176e-04},
{-8.56716727659588957e-05, 9.74478786593559361e-04, -1.65892838405139512e-03},
{6.53468478750158263e-04, 7.51662000672516676e-04, -6.73196326298856570e-04},
{-4.42539011000103941e-02, -2.01965359697350230e-02, -9.94663493761314355e-01},
{-7.39107395392403087e-04, 5.28870828612476996e-04, 1.00947183860234540e-03},
{-2.06577300933763214e-03, 9.60215813758718011e-04, -3.27993888180819421e-04},
{3.47783280638377555e-04, 8.41824316850705743e-04, -8.87458944147930993e-04},
{1.20960551709587905e+00, -7.07660818059813873e-02, -8.56332806008946491e-03},
{2.11116509318935269e-04, 7.68490846994171776e-04, -1.63228995491542417e-03},
{6.47698075356516103e-04, -4.20589129268072884e-04, 1.18354001300614896e-03},
{-2.78795945253848716e-02, 1.22199201000304547e+00, -2.07383075858847743e-01},
{-5.32457386680677347e-05, -9.58027320315790677e-04, 9.89667309649038679e-04},
{-9.03932426306289782e-02, -4.00969232187064692e-02, 1.26285611182120072e+00},
{-2.19453630740322871e-03, -1.21893190049422620e-03, -1.92293368093085417e-03},
{1.72950845415964505e-06, -8.93952511560151819e-09, -6.14874900641340649e-06},
{8.02644554976326974e-06, -6.42543741723487294e-06, -6.07103419227907060e-06},
{3.20307552755319525e-06, -4.83533743093466500e-06, 9.13563764113473065e-07},
{-2.18105804067510178e-06, 6.19595552598436322e-07, 5.21392855381760945e-06},
{-2.43310123604345563e-06, 2.17201813434465818e-06, 1.94098874242362718e-07},
{-1.56293672065252465e-06, 3.95256011818110372e-06, 1.68792962079201969e-06},
{-1.37567295252127852e-03, 3.59746071987262106e-04, 7.38927139000157259e-05},
{4.27822004137219658e-06, -8.80187479967658548e-07, 2.29453131891411977e-06},
{7.68758937964332534e-06, 2.40909410585557829e-07, 4.69351234070854509e-06},
{-2.87166709944317033e-06, 7.60223902901142716e-07, 4.57864913314467992e-06},
{-4.01295140267654560e-06, 2.65929275888376483e-06, -2.36575067819565221e-06},
{2.32693030513910805e-07, 2.28814396769890308e-06, 1.83526107699893970e-07},
{-2.18213927011287265e-03, 1.65013083920367864e-03, 2.31992998847323087e-04},
{-7.70829764693697905e-06, 4.23888841240673345e-07, 7.30018322002944087e-06},
{-1.23111329452911533e-06, 1.50076529718910084e-06, -1.91139744928209288e-06},
{-1.68872756433485760e-06, 1.03254236824697979e-06, -1.72081108163607555e-06},
{1.64276928199709460e-06, -4.96350219553231067e-07, -1.46349385185670297e-06},
{1.12731767057843682e-03, 5.03104281148445223e-04, 1.36398977654308994e-03},
{-1.05449609518089293e-06, -4.06952115309007489e-07, 3.53062441379482783e-06},
{-1.98745923822574166e-06, 4.98021943693208180e-07, 3.92645061370218429e-06},
{-1.55569377977005097e-07, -4.00262856484093037e-07, -2.49609122397048688e-06},
{2.18005022830924673e-03, -4.10275057064835439e-05, -2.59776311836759947e-04},
{5.41337439827552225e-07, -1.88603932528607146e-06, -2.06428606152470051e-06},
{-6.03243799807140491e-06, -3.75067864464502022e-06, -3.05702776851046742e-06},
{2.30038011634901016e-03, -1.32581161861259635e-03, -1.07680096899188406e-03},
{4.46773877910556887e-06, 1.85008408528524772e-08, -2.72851357570281713e-06},
{-1.49177636513049289e-03, -1.91426739654176659e-04, -1.71206384332753194e-03},
{2.31661325589237743e-02, 2.26540538563063554e-01, 5.42330337046266139e-02},
{-1.40563059963100256e-06, -4.50551806294901061e-06, 8.87542894832671347e-06},
{-1.66780916452391459e-06, 4.12065434881171526e-06, -3.55865035776836702e-06},
{2.71536622051954390e-07, -3.08564858926584692e-06, -1.52164363662402047e-06},
{2.66659632027280158e-06, -1.19436686895073481e-06, -3.25738306279285683e-06},
{-1.43666282346327501e-06, -2.51923473623639690e-06, 5.21205120344175876e-06},
{2.82954522469612199e-04, -1.59147454710008968e-03, 1.27685773978167098e-03},
{-3.99471240294241303e-06, 9.97323772325767188e-08, -5.28196823261495307e-06},
{-6.39858432699424995e-06, -4.59897864440506933e-06, -2.39736149785715891e-06},
{2.89457420106498109e-06, -3.10427512149489757e-06, 9.75553221437691631e-07},
{-8.96518259720091581e-07, -5.53996694461914366e-06, 1.03733964032237669e-05},
{8.82130497168875905e-04, -2.33618402105562365e-03, 1.35100410641244379e-03},
{-2.14088521029685841e-06, 2.59005410360388117e-06, -9.78713171504927426e-08},
{-4.50668337071552516e-06, 3.58808570076458002e-06, -1.56159349007541082e-06},
{-1.52345101244247272e-06, 2.21066768791959578e-06, -2.19555898547246775e-06},
{2.07334042074768356e-03, -1.56333498489329517e-03, -5.53762940364141767e-04},
{2.22151748134440108e-06, -4.74729938900429749e-07, -3.46744150304684889e-06},
{2.95389009221172505e-06, -2.96312023445686329e-06, -9.00385068308695580e-07},
{-6.47780848348620771e-04, 2.38772263398574292e-03, -8.93908589731968019e-04},
{9.69501567645025819e-07, 2.41432205872957328e-06, 5.56908291093893837e-07},
{-6.33392066185247586e-04, 2.38613844267241120e-03, -1.05383725637261472e-03},
{6.76250135616376785e-02, -5.57799579151454852e-02, 1.83393652374666566e-01},
{3.53986894266120067e-06, 5.92996717102502093e-06, -7.32378536156402804e-06},
{5.69667193362453916e-06, 1.20219201908705218e-06, -4.56663805956276925e-06},
{7.11494218295222192e-07, 2.93069858359131137e-06, 1.23210839732268429e-07},
{-3.41917893741799928e-06, -1.47435291776966751e-06, 1.07397354370819542e-06},
{7.30931882734254710e-04, 1.15433149094644884e-03, -2.40026982569019722e-03},
{-1.22780859907432871e-06, 2.29287908084027789e-06, 1.84270754640877832e-06},
{7.71579140080615178e-07, 2.92378122615943208e-06, -1.91800935486416413e-07},
{-3.76107279903559188e-07, -1.83159743461489867e-06, 8.17089655984204466e-07},
{-1.10830882430058061e-03, -5.10908079549339251e-04, -1.77835176235151705e-03},
{-1.26839781743699406e-06, -2.86942252006448415e-06, 4.47464983859263005e-06},
{-1.44518716284694482e-06, -7.03360635528004451e-06, 1.04898109513258675e-05},
{-4.98687888007460470e-04, 1.86990180752567262e-03, -1.24341018156770089e-03},
{-2.90479801332704790e-06, -9.24272269110706229e-07, 7.56354222045119151e-07},
{-1.16451534008294149e-03, -2.34216801827852273e-03, 4.91479264672447288e-03},
{-7.70970926241258958e-02, 9.35855573900774423e-02, 1.50623807158846906e-01},
{1.14039905307547484e-06, -1.80664235182388840e-07, -5.15527441317074897e-06},
{7.50559587697416375e-06, -6.23982034686780714e-06, -5.01245198064126721e-06},
{2.37840954889385892e-06, -4.15663063190341991e-06, 1.93118829429697603e-06},
{-1.54903048110950777e-03, 2.65832194444263125e-04, 5.34401520444913940e-04},
{4.00040634507183718e-06, -2.43965474694277443e-06, 2.88683251413283937e-06},
{7.72301916160400559e-06, -9.54300275625495457e-07, 5.50777546561020959e-06},
{-2.28103126593574368e-03, 1.02658341009706066e-03, 1.22010567464172614e-03},
{-6.32818026002207601e-06, 9.83088209200334157e-07, 5.24316808343458507e-06},
{1.37175660779395581e-03, 4.01188715721313943e-04, 7.59370199245276625e-04},
{-3.33184694847917573e-01, 7.82846225823195241e-02, -9.94270054263078074e-02},
{-1.70108770909324636e-06, -5.10749831734438279e-06, 9.80267482880020635e-06},
{-1.79301365419055891e-06, 4.44839673308561508e-06, -3.83837422072638712e-06},
{1.71911692904483371e-04, -1.56077480341044431e-03, 1.30725115579017584e-03},
{-3.55763938679129477e-06, 1.20558966207589408e-06, -5.94340114624253291e-06},
{1.02325453537648178e-03, -1.52640960762801372e-03, 3.10973117856692537e-04},
{3.81842873295820109e-03, -3.02114884453467680e-01, 2.78264587142456665e-01},
{3.46123498726202961e-06, 5.05929187103208375e-06, -6.85764673719752027e-06},
{4.47228353489932293e-04, 9.60672217798415784e-04, -2.19382758010531077e-03},
{2.22711833124298791e-01, -4.14141995162802465e-02, -4.27998216564745015e-01},
{-1.78271151817048783e-03, -9.81039111371464307e-04, -1.37513011841553174e-03},
{3.35305394560947434e-10, -1.26710751613412498e-09, 3.54248685940916630e-09},
{-9.26917423371698135e-09, 6.21190912597491263e-09, 1.86942252233812667e-08},
{-1.56687696151180944e-09, -5.44315731376698864e-09, 1.93822974337010123e-09},
{7.52897716393974292e-10, -3.48923168136394679e-10, -5.94217786087369859e-10},
{2.52116855170569920e-10, -2.48216903975251313e-09, 1.01699001303634518e-09},
{3.72215577457146729e-09, 4.51910314724912610e-10, -6.15361639422218332e-09},
{-2.62088816666700142e-07, 3.23631086683010168e-07, 8.85302852722882894e-07},
{-1.30537319842360944e-08, 1.46808588619151692e-08, 2.67574040702101001e-09},
{-1.23991327621864045e-08, 2.61298349069072344e-08, -4.58919307373337193e-09},
{5.03079244928983371e-09, -6.73783119575777079e-10, -1.13935871848269699e-08},
{9.09065785148488459e-09, -1.04304054004966673e-08, -3.23123813816827976e-09},
{9.55627910137479830e-10, -1.41129563591135820e-08, -1.75594400131373618e-09},
{-1.05549669436946769e-07, 8.47284096194811896e-08, 6.70761880091491625e-07},
{-5.92079330008488114e-10, 6.31702118392141188e-09, -4.51534448719925763e-09},
{-1.04033970327321867e-09, 4.67775485013532943e-09, 2.79348504744758586e-09},
{5.38758108958869997e-09, -9.55380699552144108e-09, 6.16488249338686956e-11},
{1.12057409185073453e-09, -3.00645183748393663e-09, -2.14940637510707688e-09},
{-6.27004681934967278e-07, 8.59159786402940127e-07, 2.73192537668387470e-07},
{7.36784189214745311e-10, -8.12761968838060511e-10, -2.43226564583531868e-09},
{1.25546123497244366e-09, -6.98609614602219153e-10, -5.29894812750786315e-09},
{-8.88351475714088679e-10, 1.37132565025677167e-09, 1.92497813869541012e-09},
{6.10992637326349119e-07, -6.13496367368217277e-07, -2.19901889726877020e-06},
{-8.59090437677068053e-11, 2.72772732179404898e-09, 1.54554039011323141e-09},
{-4.58798915525804318e-10, 4.54384851966693759e-09, 3.63189350816028877e-09},
{9.93115786933340683e-08, 1.63700862245048928e-07, -1.71397937400244449e-07},
{-1.62985361318312982e-09, -3.10762126448649312e-09, 1.76193495557419588e-09},
{6.27207737564569601e-07, -1.49343052365004934e-06, 8.16168870109573730e-08},
{1.42518738380244172e-03, -3.47531891583186285e-04, -2.98661838800559913e-04},
{8.98157254125564464e-09, -8.24242643235328920e-09, -5.34769730234363472e-09},
{-2.17776999489327494e-08, -4.47141107473569832e-09, -1.10218517090920898e-08},
{3.19614509858290319e-09, -3.32861183754973311e-09, 9.92016746526047655e-11},
{-2.91660393059167689e-09, 5.59829099744391101e-09, 1.70080685646389895e-09},
{1.22479524179014421e-09, 9.20737683318684219e-09, -1.10618757209746121e-10},
{7.70594587548882257e-09, -1.33267446898667659e-06, 4.52812675308736368e-07},
{9.46080642993951670e-09, -1.95483249032513129e-08, -1.23592694620255905e-08},
{-2.02330094345448686e-09, 1.18198534293512125e-10, 2.34746776184291406e-09},
{4.00839940406516604e-09, -4.80716730311137042e-09, 5.25802457129742606e-09},
{-2.53115202408782380e-09, 2.05563177591017165e-10, 5.46003270374129102e-09},
{3.24841319972028232e-08, -1.24284705839720552e-06, 4.97326549863015555e-07},
{1.37729661009444726e-09, -1.67903983772088594e-09, -5.62083748989472554e-09},
{-3.53256937590806785e-10, 4.49320892992322030e-09, -4.02300486673778934e-09},
{2.48976475547557641e-09, -6.97256366533061112e-09, 1.43185084622299286e-09},
{-4.38617299338556199e-09, 9.45081248826811111e-08, -2.91197460585562728e-07},
{3.24429103026879773e-09, -1.71647943601749287e-09, 2.71076100455402980e-09},
{3.86933235105302309e-09, -2.82628156988984358e-09, 8.24455756442965537e-09},
{-7.46614068323353530e-07, 1.27696340529665289e-06, 6.88413034833322557e-07},
{-5.78118683480788320e-09, 1.34319005917760137e-09, -1.15898873831454807e-09},
{4.42686972671260670e-07, 6.41810588767341775e-07, -1.16058405342719939e-08},
{2.24399192788231686e-03, -1.35129336477888174e-03, -7.39944244498236844e-04},
{7.47869199901884940e-09, -2.68762612165573955e-09, -7.41584788022109365e-09},
{1.80867308283150230e-09, -2.21500551234043996e-09, 1.86995768869380186e-09},
{-5.05514829302056157e-09, 4.74048706539109688e-09, 2.52998993977016085e-09},
{1.32441967115592973e-09, 5.70339246663831290e-09, 7.13448300437846683e-10},
{1.19767475292940212e-06, 6.72445227582811568e-07, -1.97500319605841551e-06},
{-1.70612399208458498e-09, 1.07145120553653328e-09, 1.73225882249550267e-09},
{1.15369127445807962e-09, -5.80362996549510513e-09, 9.33515653667171819e-10},
{3.38692740520230018e-09, 3.72531013675958533e-09, -3.18062756687886861e-09},
{1.14787653780236421e-06, -1.84917201319622368e-06, -2.44834286920736499e-07},
{1.45558928799083276e-09, 1.12720083267348059e-09, 9.00940544390493869e-10},
{2.09654001104286891e-09, 4.92913422578400429e-09, 3.04938074791039071e-10},
{3.54033623213741155e-07, 1.07259516691213860e-06, -6.03027205987524684e-07},
{-2.72038239157446071e-09, -1.60070143945256760e-09, 6.03853855807301443e-10},
{-2.03235662485238069e-06, -1.03151962834260348e-06, 1.99637918628457062e-06},
{-1.26261175077493210e-03, -4.98503988506484859e-04, -1.03875859619143593e-03},
{6.43182729298530376e-10, 8.01776645076301975e-10, -1.83589794755523172e-09},
{4.01805119037978997e-09, -5.63673552278487477e-10, -1.09102650663883693e-08},
{-1.48648961195707585e-09, 5.01067861508053269e-09, 2.99132781045319263e-09},
{-8.91404754824534629e-07, 7.49163968581634775e-07, 2.12542215183124383e-06},
{2.38642574451608525e-09, -3.47605810802065207e-09, 3.86935566920598717e-10},
{-2.80031986488182838e-09, -4.25160427697246490e-11, 2.24182921879090280e-09},
{-1.26991357818351247e-07, -1.45348284568834647e-07, 5.68792533226815389e-07},
{1.39227229745131353e-09, -1.84849578699353145e-09, 2.24967258190267305e-09},
{-1.15462500328497586e-06, 1.84347590761761086e-06, 3.64918716654494962e-07},
{-2.09357112083411985e-03, 1.60820400301404873e-05, 2.27418117008655948e-04},
{-1.04484803378768198e-08, 4.86043558178828050e-09, 2.00996588123336650e-09},
{1.44040971927772432e-08, 1.42223015309195233e-09, 1.99778974613318283e-09},
{-1.62414574166394599e-07, -1.31976785339561840e-06, 4.43918084507000099e-07},
{3.73061943836905385e-09, 1.00036822436866402e-08, -1.05450977117005351e-09},
{-2.06551932971539565e-07, -9.72167971235462190e-07, 4.28861904300768815e-07},
{-2.16051814014425313e-03, 1.48780488507118812e-03, 7.79940397419977911e-04},
{-4.80544204428667854e-09, -1.09870773590259319e-09, 6.58876991984844174e-09},
{1.31575045692056136e-06, 4.32430764481131318e-07, -1.55255090541518703e-06},
{1.28823975640215602e-03, 4.04521283440268135e-04, 1.76186984141882253e-03},
{-1.09767251093991436e-01, -4.94112205838347640e-02, -5.43102978164306804e-02},
{7.93691223854864347e-10, 1.54639511196208446e-08, -1.71518303448969789e-08},
{2.56523843833456056e-09, -2.31047392329486456e-09, -4.29758133398648601e-09},
{-9.87725901069325118e-09, 4.28127375218245732e-09, 2.02888056355376989e-09},
{3.21762172461603768e-10, -5.82937505211322815e-09, 3.88293127512318037e-09},
{1.63250610252241302e-09, -7.02161705168347083e-09, 3.46592492032893329e-09},
{-1.44272117683086343e-07, -4.40408510988914148e-07, 5.92746408872857344e-07},
{2.71961467235293242e-09, -1.47466668633244868e-08, 2.89637452632884873e-08},
{1.47637712476396399e-08, 1.16406781783262581e-09, 2.04904540557215853e-09},
{-5.53709807865621073e-09, 7.05512286092169205e-09, 1.56159114805820565e-09},
{5.29268649740455288e-09, 2.10616986628942016e-08, -3.03219004488264332e-08},
{1.79978890693655025e-07, 7.95085399132693105e-07, -4.78366567607801940e-07},
{-4.03847393894152251e-10, 2.90357085597214848e-09, 1.12992165623992946e-09},
{2.99031871486832301e-09, -1.37951879780606745e-09, 2.41048263988075107e-09},
{1.26882357398550027e-09, 1.30631467101793852e-09, 7.99574240151201820e-10},
{-1.41169562567489137e-08, 1.27148955713198356e-06, -2.89386439707162157e-07},
{-2.68794415198003733e-09, 8.73673404455654889e-10, 2.89557382238125882e-09},
{-4.90264437380538709e-09, 1.89207244316591527e-09, 2.25393465003165261e-09},
{-3.58274654665979853e-08, 2.91386646529383231e-07, -4.98477764412919022e-08},
{1.65722165851311942e-09, -1.11673743863338615e-09, -4.14131162695952071e-09},
{-1.47751280626939874e-07, -2.41471865000848773e-07, -8.53552350049691100e-07},
{-2.24352957583577790e-04, 1.60900273524284708e-03, -1.32260753549593617e-03},
{2.05497643901431104e-09, 1.38702982710459111e-08, -3.09887516689033582e-09},
{3.39770491949997755e-09, 9.41613393506957053e-09, -7.09844738544518350e-10},
{7.86209687630989862e-10, 1.93556837224662104e-10, -6.58630930350234678e-09},
{-6.86841181152253455e-10, -5.57194149153339424e-09, 1.41214109156129197e-09},
{2.59516074158083754e-07, 1.30703181255419770e-06, -4.02454784192984860e-07},
{-5.79425202262839889e-10, 4.05071760856134944e-09, 3.02384985106929349e-09},
{4.00677924866643664e-09, -2.25614611715219127e-09, 7.52819043214891792e-09},
{2.34003759425061020e-09, 5.27462258592681366e-09, -2.05723854618256041e-10},
{2.29340174767722615e-07, 1.05507868574435809e-06, -4.45904844964539748e-07},
{-3.91634245866523401e-09, 1.07849931763048801e-09, 1.85542686770290288e-09},
{-6.62166513287765213e-09, 3.86355018811013196e-09, -1.87861701195224384e-09},
{1.32112240848469842e-07, 4.39339645861430705e-08, -1.59384598983486336e-06},
{2.02488462108796341e-09, -1.48427112267590644e-09, -4.32055485832805175e-09},
{-4.27701540045566375e-07, -1.46229443391283215e-06, -2.38186369433401879e-07},
{-9.86744509368740232e-04, 1.91104095070606826e-03, -8.17774843405986713e-04},
{2.06891823117949514e-10, -2.64060942556376688e-09, 1.86419366055012858e-09},
{8.33785634979378187e-09, -1.00697171434571686e-08, -2.84106664583116952e-09},
{5.07057938692323518e-09, -9.56246298811080919e-09, -6.33399999117045809e-11},
{-6.78808357162941078e-08, -2.21612941845184680e-07, 9.42031624998063144e-08},
{-3.04300065007145903e-09, 5.64120231083542478e-09, 1.65718606892628628e-09},
{3.76240642807612602e-09, -4.58941407446844529e-09, 5.06162500801821125e-09},
{7.25149885354159363e-07, -1.18149759075966698e-06, -6.82406347277120240e-07},
{-4.84358128605144600e-09, 4.56893046833772853e-09, 2.67044331092591847e-09},
{-2.54939737986958903e-07, -1.06106228658746360e-06, 5.04013386790069795e-07},
{-2.17097468872509735e-03, 1.41624400187313607e-03, 8.11305605779899562e-04},
{2.24635331169675823e-10, -6.02144184513875302e-09, 4.15827878380570226e-09},
{-4.55408258326350790e-09, 6.20319154376325343e-09, 2.08760821823750220e-09},
{2.10871853867367065e-07, -4.29346688506603014e-07, 1.15683623843482186e-07},
{1.00732072683129559e-09, 3.88267751283422058e-11, -6.73798626615873530e-09},
{5.34506627847264326e-09, -8.01262819982717645e-08, 1.60888846226225901e-06},
{5.83419066552946048e-04, -2.36474094848551555e-03, 8.79373865688287898e-04},
{-4.85158746510450101e-10, -6.78789624508624456e-09, 4.95385649168511577e-09},
{3.47485142271342085e-07, 5.60944792101468470e-07, -4.35887910682497548e-07},
{5.75824910919892421e-04, -2.18618554413632388e-03, 1.22736498224538170e-03},
{-2.51838883195707221e-02, -8.23487774284355212e-02, 3.33658831723806573e-02},
{-8.70167529698484543e-09, -1.37080219501928280e-08, 1.80728228771354082e-08},
{-4.67111571644807100e-09, -2.72041008123058425e-09, 7.06648883852523113e-09},
{7.26183221906172727e-10, -6.77816339167414128e-09, 4.52883232651690726e-09},
{5.28852302228433047e-09, 6.47161005340457507e-09, -8.67298467766008940e-09},
{-2.25465519365641853e-07, -6.46057585221293529e-07, 3.48151143400587948e-07},
{-1.30051025504229756e-09, -3.25062288891730944e-09, 2.01775679498084060e-09},
{-5.12724809831333062e-09, 9.33902577666956280e-10, -6.96327353416625883e-10},
{-3.10810940873373909e-09, -7.49756534634826721e-10, 6.87357185058523612e-10},
{-1.52109221995821997e-06, -4.22908767925417317e-07, 1.38629667568307413e-06},
{1.42955317028459206e-09, -7.02968461219199980e-10, -3.81617160094549490e-09},
{2.53707400921232562e-09, -1.60727622877665510e-09, -4.18765366827500429e-09},
{-2.14750738948554787e-07, -6.40554276953864132e-07, 3.76128531993924486e-07},
{3.83073214815787821e-09, 4.50296289838947317e-10, 2.29523194894554194e-09},
{4.76340728555735282e-07, 6.83235613037347367e-07, -4.72205395646296822e-07},
{-6.10651996176347607e-04, -1.06790499934057291e-03, 2.29083496655867842e-03},
{3.95497823379997726e-09, 1.38236928154400474e-09, -6.26218820548585242e-09},
{1.11904936705986557e-09, -1.37869946362223494e-08, -9.34049783699042457e-10},
{1.25499246411697740e-09, -2.73635453185150368e-09, -2.91506864740637139e-09},
{-3.59882924006599270e-07, 1.32511373732895413e-06, -1.55110207063907657e-07},
{1.07068498511608823e-09, 8.92087770321126072e-09, 2.62826524433101838e-10},
{-2.69316546841480431e-09, 9.61138280075601870e-10, 5.19946977139973399e-09},
{-5.92563579700916554e-07, -1.05071339539294234e-06, 1.56249964602256375e-07},
{1.32198180180509439e-09, 5.16087961255351502e-09, 8.46339526239248130e-10},
{2.07323220008381881e-06, 1.02309267446332522e-06, -2.07661522726165781e-06},
{1.31402366846389393e-03, 3.78229792813366064e-04, 1.77496793932758741e-03},
{8.59301428624004160e-10, -6.83071707530125138e-09, 3.36249680876754553e-09},
{5.27310424491833629e-09, 2.09999085065692981e-08, -3.10459945807028959e-08},
{-8.88666080375855039e-08, 4.60897593930476024e-07, 7.41576575386676540e-07},
{-4.85540663230921155e-10, -5.58243438975036810e-09, 7.40450811775872353e-10},
{4.03141117225058743e-07, 1.52035531639227450e-06, 9.06206514897367477e-08},
{5.61075629915620496e-04, -2.05847905628765053e-03, 1.12849817492909434e-03},
{5.11216541321246609e-09, 7.26292920250060092e-09, -8.97145741030058730e-09},
{-4.26211688914213127e-07, -7.03366608210270750e-07, 6.27995585866791828e-07},
{1.15309052943982646e-03, 2.34474318844151959e-03, -4.91856748507475423e-03},
{1.01104427799588961e-01, -4.22361682938472982e-02, -1.88750007538552200e-01},
{3.94738332298860684e-10, -7.81372397340440727e-10, 4.06815717224340290e-09},
{-8.61483928638051566e-09, 5.37427180535843263e-09, 1.81738104426676372e-08},
{-8.48011268844706123e-10, -5.33803143354383280e-09, 2.99703953494934172e-10},
{3.89154099408092063e-07, -2.44166311268514957e-07, -8.03240371135063858e-07},
{-1.20249536439409610e-08, 1.48908931921210019e-08, 1.88292573199966284e-09},
{-1.16401289163015065e-08, 2.57866422936903206e-08, -5.27022399332555125e-09},
{1.37065399911928676e-07, 2.16494406102361175e-08, -7.63924557662179482e-07},
{-6.94754161319199870e-10, 6.65038621394664631e-09, -4.31779645371221932e-09},
{4.72542155592614588e-07, -7.58546986886782931e-07, -2.35913417925837088e-07},
{1.46133817312113241e-03, -3.25193103208258009e-04, -3.06625181254991741e-04},
{9.35794082672593210e-09, -7.92923574022275091e-09, -5.41426242728348939e-09},
{-2.15279239157428748e-08, -4.16754339024882903e-09, -1.12896482995505920e-08},
{2.60645369870582400e-10, 1.44616071127263122e-06, -3.63334053799999057e-07},
{9.17105741349288905e-09, -2.02295233654725681e-08, -1.20002956877085509e-08},
{-1.27759226226098477e-07, 1.28193771791124470e-06, -5.83097827522305323e-07},
{2.26880791869919426e-03, -1.34042850080092401e-03, -7.65092051285704835e-04},
{7.03374036792325796e-09, -2.53508958270032281e-09, -7.66132998708535240e-09},
{-9.71978722189015265e-07, -5.57836512454779054e-07, 1.96329328074063003e-06},
{-1.26115140811304343e-03, -4.81792074617704632e-04, -1.06803272537897391e-03},
{1.19419564863885497e-01, 5.07766738901840875e-02, 4.87642090320925953e-02},
{1.14090414893297520e-09, 1.56073433760228752e-08, -1.78054684078429726e-08},
{3.03285130343056153e-09, -1.58615337531031741e-09, -4.94928394101368241e-09},
{2.64483280249840080e-07, 2.97155396291660413e-07, -5.41608085095034164e-07},
{2.68757552324139226e-09, -1.41400907649469332e-08, 2.93255796729452456e-08},
{-2.11094617584561828e-07, -6.56355695552793272e-07, 3.72180321686621518e-07},
{-2.55073452371079590e-04, 1.57943859317488818e-03, -1.29154484940938240e-03},
{1.40049266628139435e-09, 1.40747080656922208e-08, -2.58792021839981956e-09},
{-2.12330362681090179e-07, -1.30522733223815968e-06, 5.84417623253341567e-07},
{-9.33144849909676392e-04, 1.90305575962152547e-03, -8.35564417983726418e-04},
{1.81624805201406961e-02, 6.84911174969819458e-02, -2.28291882522520390e-02},
{-8.25231299961259879e-09, -1.40227519596081152e-08, 1.78809529925716415e-08},
{1.90689491530449118e-07, 7.01057736002264065e-07, -4.26430629252294580e-07},
{-5.85146839837499930e-04, -1.07311215649546045e-03, 2.31986890222730339e-03},
{-1.05962397073886522e-01, 5.51532131360410807e-02, 1.87542648909451215e-01},
{-1.37499370823599516e-03, -8.49619409242363438e-04, -1.18180356709159952e-03}
};
static const double INTERCEPT[3] = {
-1.29208772400146188e+00,
6.62251952866635918e+00,
-1.35908984683965173e-01
};
// END AUTO-GENERATED COEFFICIENTS
inline void compute_poly_features(const double x[7], double out[330]) {
for (int i = 0; i < N_FEATURES; ++i) {
double val = 1.0;
for (int j = 0; j < N_INPUTS; ++j) {
if (POWERS[i][j] != 0) {
double base = x[j];
int exp = POWERS[i][j];
// Fast integer exponentiation (max exp = 4)
double p = 1.0;
for (int e = 0; e < exp; ++e)
p *= base;
val *= p;
}
}
out[i] = val;
}
}
} // namespace detail
struct RGB {
unsigned char r, g, b;
};
/**
* Mix two RGB colors using polynomial pigment mixing.
*
* This performs polynomial pigment-style RGB interpolation.
*
* @param r1,g1,b1 First color (0-255)
* @param r2,g2,b2 Second color (0-255)
* @param t Mixing ratio: 0.0 = all color1, 1.0 = all color2
* @param out_r,out_g,out_b Output color (0-255)
*/
inline void lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) {
// Clamp t
if (t <= 0.0f) {
*out_r = r1; *out_g = g1; *out_b = b1;
return;
}
if (t >= 1.0f) {
*out_r = r2; *out_g = g2; *out_b = b2;
return;
}
double x[7] = {
static_cast<double>(r1), static_cast<double>(g1), static_cast<double>(b1),
static_cast<double>(r2), static_cast<double>(g2), static_cast<double>(b2),
static_cast<double>(t)
};
double features[330];
detail::compute_poly_features(x, features);
// Dot product: features @ COEF + INTERCEPT
for (int c = 0; c < 3; ++c) {
double sum = detail::INTERCEPT[c];
for (int i = 0; i < detail::N_FEATURES; ++i) {
sum += features[i] * detail::COEF[i][c];
}
// Clamp to [0, 255] and truncate (matches numpy astype(int) behavior)
int val = static_cast<int>(sum);
if (val < 0) val = 0;
if (val > 255) val = 255;
if (c == 0) *out_r = static_cast<unsigned char>(val);
else if (c == 1) *out_g = static_cast<unsigned char>(val);
else *out_b = static_cast<unsigned char>(val);
}
}
/**
* Convenience overload returning an RGB struct.
*/
inline RGB lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t) {
RGB result;
lerp(r1, g1, b1, r2, g2, b2, t, &result.r, &result.g, &result.b);
return result;
}
} // namespace filament_mixer
#endif // FILAMENT_MIXER_MODEL_HPP
@@ -108,6 +108,22 @@ const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
return m_cached_coefficients;
}
bool CornerSmoother::is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next)
{
const Vec2d incoming_leg = vertex - previous;
const Vec2d outgoing_leg = next - vertex;
const double incoming_length = incoming_leg.norm();
const double outgoing_length = outgoing_leg.norm();
// A vertex repeating one of its neighbours carries no direction of its own.
if (incoming_length < EPSILON || outgoing_length < EPSILON)
return true;
const Vec2d incoming = incoming_leg / incoming_length;
const Vec2d outgoing = outgoing_leg / outgoing_length;
return incoming.dot(outgoing) > 0. &&
std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x()) < EPSILON;
}
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
{
m_corner_points.clear();
+41 -18
View File
@@ -1,6 +1,7 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <vector>
@@ -47,36 +48,57 @@ public:
template<typename Emit> void push(const Vec2d &point, Emit &emit)
{
if (m_pending == 0) {
if (m_held == 0) {
// The first point of a path is an end, not a corner, and stays where it is.
emit(point);
m_previous = point;
} else if (m_pending > 1) {
round_corner(m_previous, m_corner, point);
for (const Vec2d &corner_point : m_corner_points)
emit(corner_point);
m_previous = m_corner;
m_window[m_held++] = point;
return;
}
m_corner = point;
m_pending = std::min(m_pending + 1, 2);
if (m_held > 1 && is_on_straight_run(m_window[m_held - 2], m_window[m_held - 1], point)) {
// The newest vertex only splits a straight leg, so the leg runs on to this point instead.
m_window[m_held - 1] = point;
return;
}
if (m_held < 3) {
m_window[m_held++] = point;
return;
}
// Both legs of the middle vertex are complete now, so its curve can no longer grow.
emit_corner(m_window[0], m_window[1], m_window[2], emit);
m_window[0] = m_window[1];
m_window[1] = m_window[2];
m_window[2] = point;
}
// Emits the last point of the path and prepares the smoother for a new one.
template<typename Emit> void flush(Emit &emit)
{
if (m_pending > 1)
emit(m_corner);
m_pending = 0;
if (m_held > 2)
emit_corner(m_window[0], m_window[1], m_window[2], emit);
if (m_held > 1)
emit(m_window[m_held - 1]);
m_held = 0;
}
private:
template<typename Emit> void emit_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next, Emit &emit)
{
round_corner(previous, corner, next);
for (const Vec2d &corner_point : m_corner_points)
emit(corner_point);
}
// Tells a vertex that only continues a straight leg (or repeats its predecessor) from a corner.
// A path doubling back on itself is not one, that vertex is a hairpin and stays where it is.
static bool is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next);
// Fills m_corner_points with the points replacing the corner vertex.
void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next);
// Flattens the canonical corner curve of the given size and turn into coordinates of the
// (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner.
const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing);
// Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment
// is the maximum, otherwise the curves of two adjacent corners would overlap.
// Fraction of the shorter adjoining leg consumed on each side of a corner. Half of a leg is the
// maximum, otherwise the curves of two adjacent corners would overlap.
const double m_corner_distance_ratio;
const double m_tolerance;
const double m_max_corner_distance;
@@ -88,10 +110,11 @@ private:
double m_cached_cosine { 0. };
bool m_has_cached_coefficients { false };
Vec2d m_previous { Vec2d::Zero() };
Vec2d m_corner { Vec2d::Zero() };
// Number of points held back: none, the first point of a path, or a corner candidate.
int m_pending { 0 };
// The corners seen last, kept free of vertices that merely split a straight leg. The middle one
// is rounded once the third arrives, which is what makes its outgoing leg final.
std::array<Vec2d, 3> m_window { Vec2d::Zero(), Vec2d::Zero(), Vec2d::Zero() };
// How many of them are filled in.
int m_held { 0 };
};
// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone.
+9 -5
View File
@@ -351,19 +351,23 @@ void Node::convertToPolylines(Polylines &output, const coord_t line_overlap) con
{
Polylines result;
result.emplace_back();
convertToPolylines(0, result);
// Orca: the layers are filled in parallel, so they would consume a shared generator in a
// different order every run, and a model would not slice the same way twice. Each tree seeds
// its own from where it is rooted; one constant seed would start them all on the same pick.
std::mt19937_64 rng { uint64_t(PointHash{}(m_p)) };
convertToPolylines(0, result, rng);
removeJunctionOverlap(result, line_overlap);
append(output, std::move(result));
}
void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
void Node::convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const
{
if (m_children.empty()) {
output[long_line_idx].points.push_back(m_p);
return;
}
size_t first_child_idx = rand() % m_children.size();
m_children[first_child_idx]->convertToPolylines(long_line_idx, output);
const size_t first_child_idx = rng() % m_children.size();
m_children[first_child_idx]->convertToPolylines(long_line_idx, output, rng);
output[long_line_idx].points.push_back(m_p);
for (size_t idx_offset = 1; idx_offset < m_children.size(); idx_offset++) {
@@ -371,7 +375,7 @@ void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
const Node& child = *m_children[child_idx];
output.emplace_back();
size_t child_line_idx = output.size() - 1;
child.convertToPolylines(child_line_idx, output);
child.convertToPolylines(child_line_idx, output, rng);
output[child_line_idx].points.emplace_back(m_p);
}
}
+3 -1
View File
@@ -7,6 +7,7 @@
#include <functional>
#include <memory>
#include <optional>
#include <random>
#include <vector>
#include "../../EdgeGrid.hpp"
@@ -259,8 +260,9 @@ protected:
*
* \param long_line a reference to a polyline in \p output which to continue building on in the recursion
* \param output all branches in this tree connected into polylines
* \param rng the generator the junctions draw from, carried through the recursion
*/
void convertToPolylines(size_t long_line_idx, Polylines &output) const;
void convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const;
void removeJunctionOverlap(Polylines &polylines, coord_t line_overlap) const;
+327
View File
@@ -0,0 +1,327 @@
#include "AssimpImport.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include <assimp/Importer.hpp>
#include <assimp/config.h>
#include <assimp/material.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <array>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
namespace Slic3r {
namespace {
void clear_textured_mesh(TexturedMesh& out)
{
out.vertices.clear();
out.indices.clear();
out.uvs.clear();
out.uv_coords.clear();
out.uv_indices.clear();
out.textures.clear();
out.material_ids.clear();
out.material_texture_map.clear();
out.material_colors.clear();
}
void set_error_message(std::string* error_message, const std::string& message)
{
if (error_message)
*error_message = message;
}
bool is_fbx_path(const std::string& path)
{
return boost::algorithm::iends_with(path, ".fbx");
}
bool should_flip_uvs(const std::string& path)
{
return boost::algorithm::iends_with(path, ".fbx") ||
boost::algorithm::iends_with(path, ".glb");
}
unsigned int assimp_import_flags(const std::string& path)
{
unsigned int flags = aiProcess_Triangulate
| aiProcess_GenNormals
| aiProcess_PreTransformVertices
| aiProcess_SortByPType;
if (should_flip_uvs(path))
flags |= aiProcess_FlipUVs;
return flags;
}
void configure_importer(Assimp::Importer& importer, const std::string& path, unsigned int flags)
{
importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE,
aiPrimitiveType_POINT | aiPrimitiveType_LINE);
if (flags & aiProcess_PreTransformVertices)
importer.SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true);
if (is_fbx_path(path)) {
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
}
}
bool read_external_texture_file(const boost::filesystem::path& path, TextureImage& out)
{
boost::nowide::ifstream file(path.string(), std::ios::binary | std::ios::ate);
if (!file.is_open())
return false;
const std::streamoff size = file.tellg();
if (size <= 0)
return false;
if (static_cast<uintmax_t>(size) > static_cast<uintmax_t>(std::numeric_limits<size_t>::max()))
return false;
file.seekg(0);
out.width = -1;
out.height = -1;
out.channels = 0;
out.data.resize(static_cast<size_t>(size));
file.read(reinterpret_cast<char*>(out.data.data()), size);
if (!file && !file.eof()) {
out.data.clear();
return false;
}
return true;
}
bool read_embedded_texture(const aiTexture& texture, TextureImage& out)
{
out.data.clear();
if (texture.mHeight == 0) {
if (texture.mWidth == 0)
return false;
out.width = -1;
out.height = -1;
out.channels = 0;
out.data.assign(
reinterpret_cast<const unsigned char*>(texture.pcData),
reinterpret_cast<const unsigned char*>(texture.pcData) + texture.mWidth);
return !out.data.empty();
}
if (texture.mWidth == 0 || texture.mHeight == 0)
return false;
if (texture.mWidth > static_cast<unsigned int>(std::numeric_limits<int>::max()) ||
texture.mHeight > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
return false;
}
const size_t width = static_cast<size_t>(texture.mWidth);
const size_t height = static_cast<size_t>(texture.mHeight);
if (width > std::numeric_limits<size_t>::max() / height ||
width * height > std::numeric_limits<size_t>::max() / 4) {
return false;
}
out.width = static_cast<int>(texture.mWidth);
out.height = static_cast<int>(texture.mHeight);
out.channels = 4;
const size_t pixel_count = width * height;
out.data.resize(pixel_count * 4);
for (size_t i = 0; i < pixel_count; ++i) {
const aiTexel& texel = texture.pcData[i];
out.data[i * 4 + 0] = texel.r;
out.data[i * 4 + 1] = texel.g;
out.data[i * 4 + 2] = texel.b;
out.data[i * 4 + 3] = texel.a;
}
return !out.data.empty();
}
bool get_material_texture(const aiMaterial& material, aiString& texture_path)
{
if (material.GetTextureCount(aiTextureType_DIFFUSE) > 0 &&
material.GetTexture(aiTextureType_DIFFUSE, 0, &texture_path) == AI_SUCCESS) {
return true;
}
if (material.GetTextureCount(aiTextureType_BASE_COLOR) > 0 &&
material.GetTexture(aiTextureType_BASE_COLOR, 0, &texture_path) == AI_SUCCESS) {
return true;
}
return false;
}
std::array<float, 4> get_material_color(const aiMaterial& material)
{
aiColor4D color(1.f, 1.f, 1.f, 1.f);
if (material.Get(AI_MATKEY_BASE_COLOR, color) == AI_SUCCESS)
return {color.r, color.g, color.b, color.a};
if (material.Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS)
return {color.r, color.g, color.b, color.a};
return {1.f, 1.f, 1.f, 1.f};
}
bool collect_mesh(const aiMesh& mesh, size_t& vertex_offset, TexturedMesh& out, std::string& error)
{
if (mesh.mNumVertices > static_cast<size_t>(std::numeric_limits<int>::max()) - vertex_offset) {
error = "Assimp mesh has too many vertices for TexturedMesh indices";
return false;
}
for (unsigned int i = 0; i < mesh.mNumVertices; ++i) {
const aiVector3D& v = mesh.mVertices[i];
out.vertices.push_back({v.x, v.y, v.z});
if (mesh.HasTextureCoords(0)) {
const aiVector3D& uv = mesh.mTextureCoords[0][i];
out.uvs.push_back({uv.x, uv.y});
} else {
out.uvs.push_back({0.f, 0.f});
}
}
const int material_index = static_cast<int>(mesh.mMaterialIndex);
for (unsigned int i = 0; i < mesh.mNumFaces; ++i) {
const aiFace& face = mesh.mFaces[i];
if (face.mNumIndices != 3)
continue;
if (face.mIndices[0] >= mesh.mNumVertices ||
face.mIndices[1] >= mesh.mNumVertices ||
face.mIndices[2] >= mesh.mNumVertices) {
error = "Assimp mesh face index is out of bounds";
return false;
}
out.indices.push_back({
static_cast<int>(static_cast<size_t>(face.mIndices[0]) + vertex_offset),
static_cast<int>(static_cast<size_t>(face.mIndices[1]) + vertex_offset),
static_cast<int>(static_cast<size_t>(face.mIndices[2]) + vertex_offset)});
out.material_ids.push_back(material_index);
}
vertex_offset += mesh.mNumVertices;
return true;
}
void collect_materials(const aiScene& scene, const boost::filesystem::path& base_dir, TexturedMesh& out)
{
out.material_texture_map.assign(scene.mNumMaterials, -1);
out.material_colors.assign(scene.mNumMaterials, {1.f, 1.f, 1.f, 1.f});
for (unsigned int material_index = 0; material_index < scene.mNumMaterials; ++material_index) {
const aiMaterial* material = scene.mMaterials[material_index];
if (!material)
continue;
out.material_colors[material_index] = get_material_color(*material);
aiString texture_path;
if (!get_material_texture(*material, texture_path))
continue;
TextureImage image;
const aiTexture* embedded_texture = scene.GetEmbeddedTexture(texture_path.C_Str());
if (embedded_texture) {
if (!read_embedded_texture(*embedded_texture, image))
continue;
} else {
const boost::filesystem::path resolved = resource_path::resolve_external_resource_path(
base_dir, texture_path.C_Str(), "Assimp texture");
if (resolved.empty()) {
BOOST_LOG_TRIVIAL(warning) << "AssimpImport: texture file not found: "
<< texture_path.C_Str();
continue;
}
if (!read_external_texture_file(resolved, image)) {
BOOST_LOG_TRIVIAL(warning) << "AssimpImport: failed to read texture: "
<< resolved;
continue;
}
}
out.material_texture_map[material_index] = static_cast<int>(out.textures.size());
out.textures.push_back(std::move(image));
}
}
std::string scene_failure_summary(const std::string& path, const char* assimp_error)
{
std::ostringstream ss;
ss << "Assimp failed to import " << path;
if (assimp_error && assimp_error[0] != '\0')
ss << ": " << assimp_error;
return ss.str();
}
} // namespace
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message)
{
clear_textured_mesh(out);
Assimp::Importer importer;
const unsigned int flags = assimp_import_flags(path);
configure_importer(importer, path, flags);
const aiScene* scene = importer.ReadFile(path, flags);
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
const std::string message = scene_failure_summary(path, importer.GetErrorString());
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
return false;
}
if (scene->mNumMeshes == 0) {
const std::string message = "Assimp scene has no meshes: " + path;
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
return false;
}
size_t vertex_offset = 0;
for (unsigned int mesh_index = 0; mesh_index < scene->mNumMeshes; ++mesh_index) {
const aiMesh* mesh = scene->mMeshes[mesh_index];
if (!mesh || !mesh->HasPositions())
continue;
std::string mesh_error;
if (!collect_mesh(*mesh, vertex_offset, out, mesh_error)) {
const std::string message = mesh_error + ": " + path;
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
clear_textured_mesh(out);
return false;
}
}
if (out.vertices.empty() || out.indices.empty()) {
const std::string message = "Assimp extracted no valid triangles: " + path;
BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message;
set_error_message(error_message, message);
clear_textured_mesh(out);
return false;
}
collect_materials(*scene, boost::filesystem::path(path).parent_path(), out);
BOOST_LOG_TRIVIAL(info) << "AssimpImport: loaded " << out.vertices.size()
<< " vertices, " << out.indices.size()
<< " triangles, " << out.textures.size()
<< " textures from " << path;
return true;
}
} // namespace Slic3r
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace Slic3r {
struct TexturedMesh;
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr);
} // namespace Slic3r
+149 -5
View File
@@ -1,6 +1,8 @@
#include "../libslic3r.h"
#include "../Model.hpp"
#include "../TriangleMesh.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include "OBJ.hpp"
#include "objparser.hpp"
@@ -21,7 +23,7 @@
namespace Slic3r {
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message)
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl)
{
if (meshptr == nullptr)
return false;
@@ -53,9 +55,9 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
boost::filesystem::path temp_mtl_path(mtl_file);
mtl_path = temp_mtl_path;
}
auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str();
const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string();
if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) {
if (!ObjParser::mtlparse(_mtl_path, mtl_data)) {
if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) {
BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path;
message = _L("load mtl in obj: failed to parse");
return false;
@@ -98,6 +100,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
its.indices.reserve(num_faces + num_quads);
if (exist_mtl) {
obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1;
obj_info.usemtls = data.usemtls;
obj_info.face_colors.reserve(num_faces + num_quads);
}
bool has_color = data.has_vertex_color;
@@ -210,14 +213,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
}
if (meshptr->volume() < 0)
meshptr->flip_triangles();
// Hand the parsed material table back so callers can build a TexturedMesh from it.
if (out_mtl)
*out_mtl = mtl_data;
return true;
}
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in)
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl)
{
TriangleMesh mesh;
bool ret = load_obj(path, &mesh, obj_info, message);
bool ret = load_obj(path, &mesh, obj_info, message, out_mtl);
if (ret) {
std::string object_name;
@@ -232,6 +238,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me
return ret;
}
bool obj_to_textured_mesh(
const ObjInfo& obj_info,
const indexed_triangle_set& its,
const ObjParser::MtlData& mtl_data,
const std::string& obj_directory,
TexturedMesh& out)
{
if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png)
return false;
const size_t nv = its.vertices.size();
const size_t nf = its.indices.size();
// 1. Copy vertices
out.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i)
out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
// 2. Copy face indices
out.indices.resize(nf);
for (size_t i = 0; i < nf; ++i)
out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
// 3. Build per-face UV (uv_coords + uv_indices)
// OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV).
// Flip V here so downstream code works uniformly.
if (!obj_info.uvs.empty()) {
const size_t uv_face_count = obj_info.uvs.size();
out.uv_coords.resize(uv_face_count * 3);
out.uv_indices.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
if (fi < uv_face_count) {
int base = static_cast<int>(fi * 3);
out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()};
out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()};
out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()};
out.uv_indices[fi] = {base, base + 1, base + 2};
} else {
out.uv_indices[fi] = {0, 0, 0};
}
}
}
// 4. Build material list and load textures from disk
// Map: material name -> material index
std::map<std::string, int> mtl_name_to_idx;
for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i)
mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i);
const int num_materials = static_cast<int>(mtl_data.mtl_orders.size());
out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f});
out.material_texture_map.resize(num_materials, -1);
// Map: texture filename -> index in out.textures
std::map<std::string, int> png_to_tex_idx;
for (int mi = 0; mi < num_materials; ++mi) {
const std::string& name = mtl_data.mtl_orders[mi];
auto it = mtl_data.new_mtl_unmap.find(name);
if (it == mtl_data.new_mtl_unmap.end())
continue;
const auto& mtl = *(it->second);
// Material color from Kd
out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr};
// Texture from map_Kd
if (mtl.map_Kd.empty())
continue;
auto tex_it = png_to_tex_idx.find(mtl.map_Kd);
if (tex_it != png_to_tex_idx.end()) {
out.material_texture_map[mi] = tex_it->second;
continue;
}
// Resolve texture file path.
const boost::filesystem::path requested_tex_path(mtl.map_Kd);
const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ?
resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") :
resource_path::resolve_existing_relative_path_case_insensitive(
boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd");
if (tex_path.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path;
continue;
}
// Read raw file bytes
boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate);
if (!file.is_open())
continue;
auto file_size = file.tellg();
if (file_size <= 0)
continue;
file.seekg(0, std::ios::beg);
TextureImage ti;
ti.data.resize(static_cast<size_t>(file_size));
file.read(reinterpret_cast<char*>(ti.data.data()), file_size);
ti.width = -1;
ti.height = -1;
ti.channels = 0;
int new_idx = static_cast<int>(out.textures.size());
out.textures.push_back(std::move(ti));
png_to_tex_idx[mtl.map_Kd] = new_idx;
out.material_texture_map[mi] = new_idx;
}
// 5. Build per-face material_ids from usemtls ranges
out.material_ids.resize(nf, -1);
if (!obj_info.usemtls.empty()) {
for (size_t fi = 0; fi < nf; ++fi) {
int face_idx = static_cast<int>(fi);
for (size_t k = 0; k < obj_info.usemtls.size(); ++k) {
const auto& um = obj_info.usemtls[k];
if (face_idx >= um.face_start && face_idx <= um.face_end) {
auto name_it = mtl_name_to_idx.find(um.name);
if (name_it != mtl_name_to_idx.end())
out.material_ids[fi] = name_it->second;
break;
}
}
}
}
if (out.textures.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded";
return false;
}
BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, "
<< out.textures.size() << " textures, "
<< num_materials << " materials";
return true;
}
bool store_obj(const char *path, TriangleMesh *mesh)
{
//FIXME returning false even if write failed.
+14 -2
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_Format_OBJ_hpp_
#define slic3r_Format_OBJ_hpp_
#include "libslic3r/Color.hpp"
#include "objparser.hpp"
#include <unordered_map>
namespace Slic3r {
@@ -18,6 +19,7 @@ struct ObjInfo {
std::map<std::string,bool> pngs;
std::unordered_map<int, std::string> uv_map_pngs;
bool has_uv_png{false};
std::vector<ObjParser::ObjUseMtl> usemtls; // material spans, for texture import
};
struct ObjDialogInOut
@@ -32,8 +34,18 @@ struct ObjDialogInOut
std::string lost_material_name{""};
};
typedef std::function<void(ObjDialogInOut &in_out)> ObjImportColorFn;
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message);
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr);
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message, ObjParser::MtlData *out_mtl = nullptr);
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr, ObjParser::MtlData *out_mtl = nullptr);
struct TexturedMesh;
// Build a TexturedMesh (vertices + per-face UVs + 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);
+240
View File
@@ -0,0 +1,240 @@
#ifndef slic3r_Format_ResourcePathUtils_hpp_
#define slic3r_Format_ResourcePathUtils_hpp_
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace resource_path {
inline std::string ascii_lower_copy(const std::string& value)
{
std::string lowered;
lowered.reserve(value.size());
for (unsigned char ch : value)
lowered.push_back(static_cast<char>(std::tolower(ch)));
return lowered;
}
inline boost::filesystem::path portable_path_copy(const boost::filesystem::path& value)
{
std::string portable = value.string();
std::replace(portable.begin(), portable.end(), '\\', '/');
return boost::filesystem::path(portable);
}
inline int hex_digit_value(char ch)
{
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return -1;
}
// Byte-level percent decoding. Per RFC 3986 the %XX byte stream is expected to be
// UTF-8 when produced from URIs / Assimp aiString; this function performs no
// transcoding, so callers must treat both input and output as raw UTF-8 bytes.
inline std::string percent_decode_copy(const std::string& value)
{
std::string decoded;
decoded.reserve(value.size());
for (std::size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
const int hi = hex_digit_value(value[i + 1]);
const int lo = hex_digit_value(value[i + 2]);
if (hi >= 0 && lo >= 0) {
decoded.push_back(static_cast<char>((hi << 4) | lo));
i += 2;
continue;
}
}
decoded.push_back(value[i]);
}
return decoded;
}
inline std::string strip_file_uri_prefix_copy(const std::string& value)
{
const std::string lower = ascii_lower_copy(value);
if (lower.rfind("file://", 0) != 0)
return value;
std::string path = value.substr(7);
if (ascii_lower_copy(path).rfind("localhost/", 0) == 0)
path.erase(0, std::string("localhost").size());
else if (!path.empty() && path.front() != '/')
path = "//" + path;
// file:///C:/... should become C:/..., while file:///tmp/... keeps /tmp/...
if (path.size() >= 3 && path[0] == '/' && std::isalpha(static_cast<unsigned char>(path[1])) && path[2] == ':')
path.erase(path.begin());
return path;
}
inline bool file_uri_has_remote_authority(const std::string& value)
{
const std::string lower = ascii_lower_copy(value);
if (lower.rfind("file://", 0) != 0)
return false;
const std::string path = value.substr(7);
if (path.empty() || path.front() == '/')
return false;
const std::size_t slash = path.find('/');
const std::string authority = path.substr(0, slash);
return ascii_lower_copy(authority) != "localhost";
}
inline bool looks_like_windows_absolute_path(const boost::filesystem::path& path)
{
const std::string portable = portable_path_copy(path).string();
return portable.size() >= 3
&& std::isalpha(static_cast<unsigned char>(portable[0]))
&& portable[1] == ':'
&& portable[2] == '/';
}
inline boost::filesystem::path filename_from_portable_path(const boost::filesystem::path& value)
{
const boost::filesystem::path portable = portable_path_copy(value);
return portable.filename();
}
inline boost::filesystem::path find_child_case_insensitive(
const boost::filesystem::path& directory,
const boost::filesystem::path& requested_name,
const char* context)
{
if (!boost::filesystem::exists(directory) || !boost::filesystem::is_directory(directory))
return {};
const std::string requested_lower = ascii_lower_copy(requested_name.filename().string());
std::vector<boost::filesystem::path> matches;
boost::system::error_code ec;
for (boost::filesystem::directory_iterator it(directory, ec), end; !ec && it != end; it.increment(ec)) {
if (ascii_lower_copy(it->path().filename().string()) == requested_lower)
matches.push_back(it->path());
}
if (matches.size() == 1)
return matches.front();
if (matches.size() > 1) {
BOOST_LOG_TRIVIAL(warning) << context << ": ambiguous case-insensitive resource match for "
<< requested_name << " in " << directory;
}
return {};
}
inline boost::filesystem::path resolve_existing_path_case_insensitive(
const boost::filesystem::path& requested_path,
const char* context = "resource_path")
{
const boost::filesystem::path normalized_path = portable_path_copy(requested_path);
if (normalized_path.empty())
return {};
if (boost::filesystem::exists(normalized_path))
return normalized_path;
boost::filesystem::path current;
bool initialized = false;
for (const boost::filesystem::path& part : normalized_path) {
if (part == normalized_path.root_name() || part == normalized_path.root_directory()) {
current /= part;
initialized = true;
continue;
}
if (!initialized) {
current = boost::filesystem::current_path();
initialized = true;
}
boost::filesystem::path exact = current / part;
if (boost::filesystem::exists(exact)) {
current = exact;
continue;
}
boost::filesystem::path matched = find_child_case_insensitive(current, part, context);
if (matched.empty())
return {};
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource path case-insensitively from "
<< exact << " to " << matched;
current = matched;
}
return boost::filesystem::exists(current) ? current : boost::filesystem::path();
}
inline boost::filesystem::path resolve_existing_relative_path_case_insensitive(
const boost::filesystem::path& base_dir,
const boost::filesystem::path& resource_path,
const char* context = "resource_path")
{
const boost::filesystem::path requested = resource_path.is_absolute() ? resource_path : base_dir / resource_path;
return resolve_existing_path_case_insensitive(requested, context);
}
// Resolve a resource path that originated outside our own code (e.g. a glTF/FBX
// material texture reference or a file:// URI inside a 3MF descriptor).
//
// `raw_path` is expected to be UTF-8 regardless of host platform: file URIs are
// UTF-8 by spec, and Assimp aiString uses UTF-8 internally. Cross-platform
// correctness on Windows additionally relies on the process having called
// boost::nowide::nowide_filesystem() during startup (see src/BambuStudio.cpp),
// which imbues boost::filesystem::path with a UTF-8 codecvt so that
// `path(std::string)` constructs from UTF-8 byte sequences. Callers that bypass
// the main entry point (standalone CLI tools, unit tests) must reproduce that
// setup themselves before invoking this helper.
inline boost::filesystem::path resolve_external_resource_path(
const boost::filesystem::path& base_dir,
const std::string& raw_path,
const char* context = "resource_path",
bool allow_basename_fallback = true)
{
if (raw_path.empty())
return {};
const bool remote_file_uri = file_uri_has_remote_authority(raw_path);
const std::string decoded_path = percent_decode_copy(strip_file_uri_prefix_copy(raw_path));
const boost::filesystem::path requested = portable_path_copy(boost::filesystem::path(decoded_path));
boost::filesystem::path resolved = (requested.is_absolute() || looks_like_windows_absolute_path(requested)) ?
resolve_existing_path_case_insensitive(requested, context) :
resolve_existing_relative_path_case_insensitive(base_dir, requested, context);
if (!resolved.empty())
return resolved;
if (!allow_basename_fallback || remote_file_uri)
return {};
const boost::filesystem::path basename = filename_from_portable_path(requested);
if (basename.empty())
return {};
resolved = resolve_existing_relative_path_case_insensitive(base_dir, basename, context);
if (!resolved.empty()) {
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource by basename from "
<< requested << " to " << resolved;
}
return resolved;
}
} // namespace resource_path
} // namespace Slic3r
#endif /* slic3r_Format_ResourcePathUtils_hpp_ */
+23 -13
View File
@@ -111,14 +111,19 @@ bool StepPreProcessor::isUtf8File(const char* path)
bool StepPreProcessor::isUtf8(const std::string str)
{
size_t num = 0;
int i = 0;
size_t i = 0;
while (i < str.length()) {
if ((str[i] & 0x80) == 0x00) {
const unsigned char lead = static_cast<unsigned char>(str[i]);
if ((lead & 0x80) == 0x00) {
i++;
} else if ((num = preNum(str[i])) > 2) {
// preNum() counts the leading 1 bits, and a multi-byte sequence is 2 to 4
// bytes long, so anything outside that range is not a lead byte.
} else if ((num = preNum(lead)) >= 2 && num <= 4) {
if (i + num > str.length())
return false;
i++;
for (int j = 0; j < num - 1; j++) {
if ((str[i] & 0xc0) != 0x80)
for (size_t j = 0; j < num - 1; j++) {
if ((static_cast<unsigned char>(str[i]) & 0xc0) != 0x80)
return false;
i++;
}
@@ -132,15 +137,20 @@ bool StepPreProcessor::isUtf8(const std::string str)
bool StepPreProcessor::isGBK(const std::string str) {
size_t i = 0;
while (i < str.length()) {
if (str[i] <= 0x7f) {
// char is signed here, so every byte compares <= 0x7f unless widened first.
const unsigned char lead = static_cast<unsigned char>(str[i]);
if (lead <= 0x7f) {
i++;
continue;
} else {
if (str[i] >= 0x81 &&
str[i] <= 0xfe &&
str[i + 1] >= 0x40 &&
str[i + 1] <= 0xfe &&
str[i + 1] != 0xf7) {
if (i + 1 >= str.length())
return false;
const unsigned char trail = static_cast<unsigned char>(str[i + 1]);
if (lead >= 0x81 &&
lead <= 0xfe &&
trail >= 0x40 &&
trail <= 0xfe &&
trail != 0xf7) {
i += 2;
continue;
}
@@ -586,7 +596,7 @@ Step::Step_Status Step::mesh(Model* model,
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
aPnt.Transform(aTrsf);
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
}
// BBS: copy triangles
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
@@ -712,7 +722,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle
return 0;
}
}
} catch(const Exception &e) {
} catch(const Exception &) {
return 0;
}
+43 -1
View File
@@ -4,6 +4,7 @@
#include "../Preset.hpp"
#include "../Utils.hpp"
#include "../LocalesUtils.hpp"
#include "../FilamentMixer.hpp"
#include "../GCode.hpp"
#include "../Geometry.hpp"
#include "../GCode/ThumbnailData.hpp"
@@ -246,6 +247,8 @@ static constexpr const char* BUILD_TAG = "build";
static constexpr const char* ITEM_TAG = "item";
static constexpr const char* METADATA_TAG = "metadata";
static constexpr const char* FILAMENT_TAG = "filament";
static constexpr const char* MIXED_FILAMENT_TAG = "mixed_filament";
static constexpr const char* MIXED_FILAMENT_COMPONENTS_TAG = "components";
static constexpr const char* SLICE_WARNING_TAG = "warning";
static constexpr const char* WARNING_MSG_TAG = "msg";
static constexpr const char *FILAMENT_ID_TAG = "id";
@@ -1315,6 +1318,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_end_config_metadata();
bool _handle_start_config_filament(const char** attributes, unsigned int num_attributes);
bool _handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_filament();
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
@@ -2694,6 +2698,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", load project config file successfully from %1%\n") %dest_file;
// Heal any gradient-curve slots corrupted by the legacy "|" separator collision
// (see FilamentMixer::sanitize_mixed_gradient_curve_array). The 3MF JSON itself
// is safe (";" + C-style escape), but older projects saved through the buggy
// export_selections/load_selections path may already carry single-point entries
// that fail MakerWorld's "curve needs >= 2 points" check.
if (auto* curve_opt = config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
Slic3r::sanitize_mixed_gradient_curve_array(curve_opt->values);
}
}
@@ -3511,6 +3523,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_start_config_plater_instance(attributes, num_attributes);
else if (::strcmp(FILAMENT_TAG, name) == 0)
res = _handle_start_config_filament(attributes, num_attributes);
else if (::strcmp(MIXED_FILAMENT_TAG, name) == 0)
res = _handle_start_config_mixed_filament(attributes, num_attributes);
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
res = _handle_start_config_warning(attributes, num_attributes);
else if (::strcmp(NOZZLE_TAG, name) == 0)
@@ -4684,6 +4698,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes)
{
if (m_curr_plater) {
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_ID_TAG);
std::string type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TYPE_TAG);
std::string color = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_COLOR_TAG);
std::string components = bbs_get_attribute_value_string(attributes, num_attributes, MIXED_FILAMENT_COMPONENTS_TAG);
PlateMixedFilamentInfo mixed_info;
mixed_info.id = atoi(id.c_str());
mixed_info.type = type;
mixed_info.color = color;
mixed_info.components = components;
m_curr_plater->mixed_filaments_info.push_back(mixed_info);
}
return true;
}
bool _BBS_3MF_Importer::_handle_end_config_filament()
{
// do nothing
@@ -8488,6 +8519,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
<< FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n";
}
// Mixed (virtual) filaments used by this plate. These are resolved to physical
// components before g-code statistics, so they are not present in the <filament>
// list above and are recorded separately here.
for (auto it = plate_data->mixed_filaments_info.begin(); it != plate_data->mixed_filaments_info.end(); it++)
{
stream << " <" << MIXED_FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id) << "\" "
<< FILAMENT_TYPE_TAG << "=\"" << it->type << "\" "
<< FILAMENT_COLOR_TAG << "=\"" << it->color << "\" "
<< MIXED_FILAMENT_COMPONENTS_TAG << "=\"" << it->components << "\"/>\n";
}
for (auto it = plate_data->warnings.begin(); it != plate_data->warnings.end(); it++) {
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
}
@@ -8921,7 +8963,7 @@ private:
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inital and interval = " << m_interval;
m_next_backup = boost::get_system_time() + boost::posix_time::seconds(m_interval);
boost::unique_lock lock(m_mutex);
m_thread = std::move(boost::thread(boost::ref(*this)));
m_thread = boost::thread(boost::ref(*this));
}
~_BBS_Backup_Manager() {
+14
View File
@@ -48,6 +48,18 @@ public:
};
// Mixed (virtual) filament used by a plate. Mixed filaments are virtual slots that get
// resolved to their physical components before g-code statistics, so they never appear in
// slice_filaments_info. They are recorded here separately so a plate's mixed-color usage
// can be recovered from slice_info.
struct PlateMixedFilamentInfo
{
int id{0}; // 1-based virtual filament slot id
std::string type;
std::string color; // blended display color, "#RRGGBB"
std::string components; // 1-based physical component ids, comma separated, e.g. "1,3"
};
//BBS: define plate data list related structures
struct PlateData
{
@@ -89,6 +101,8 @@ struct PlateData
std::string first_layer_time;
std::string plate_name;
std::vector<FilamentInfo> slice_filaments_info;
// Mixed (virtual) filaments used by this plate; empty when no mixed filament is used.
std::vector<PlateMixedFilamentInfo> mixed_filaments_info;
std::vector<size_t> skipped_objects;
DynamicPrintConfig config;
bool is_support_used {false};
+106 -7
View File
@@ -262,12 +262,9 @@ static bool obj_parseline(const char *line, ObjData &data)
}
face_index_count++;
}
if (face_index_count == 3) {//tri
data.usemtls.back().face_end++;
} else if (face_index_count == 4) {//quad
data.usemtls.back().face_end++;
data.usemtls.back().face_end++;
}
if (face_index_count >= 3) {
data.usemtls.back().face_end += face_index_count - 2;
}
}
vertex.coordIdx = -1;
vertex.normalIdx = -1;
@@ -374,6 +371,107 @@ static bool obj_parseline(const char *line, ObjData &data)
return true;
}
static std::string cur_mtl_name = "";
static bool mtl_is_space(char c)
{
return c == ' ' || c == '\t' || c == '\r';
}
static const char* mtl_skip_ws(const char *line)
{
while (mtl_is_space(*line))
++line;
return line;
}
static const char* mtl_skip_token(const char *line)
{
while (*line != 0 && !mtl_is_space(*line))
++line;
return line;
}
static bool mtl_token_equals(const char *begin, const char *end, const char *token)
{
const size_t len = static_cast<size_t>(end - begin);
return strlen(token) == len && strncmp(begin, token, len) == 0;
}
static std::string mtl_trim_value(const char *line)
{
const char *begin = mtl_skip_ws(line);
const char *end = begin + strlen(begin);
while (end > begin && mtl_is_space(*(end - 1)))
--end;
return std::string(begin, end);
}
static bool mtl_skip_numeric_token(const char *&line)
{
const char *begin = mtl_skip_ws(line);
if (*begin == 0)
return false;
char *endptr = 0;
strtod(begin, &endptr);
if (endptr == begin || (!mtl_is_space(*endptr) && *endptr != 0))
return false;
line = mtl_skip_ws(endptr);
return true;
}
static bool mtl_skip_required_tokens(const char *&line, int count)
{
for (int i = 0; i < count; ++i) {
line = mtl_skip_ws(line);
if (*line == 0)
return false;
line = mtl_skip_token(line);
}
line = mtl_skip_ws(line);
return true;
}
static std::string mtl_parse_texture_name(const char *line)
{
const char *original = mtl_skip_ws(line);
const char *current = original;
while (*current == '-') {
const char *option_begin = current;
const char *option_end = mtl_skip_token(current);
current = option_end;
if (mtl_token_equals(option_begin, option_end, "-o") ||
mtl_token_equals(option_begin, option_end, "-s") ||
mtl_token_equals(option_begin, option_end, "-t")) {
int skipped = 0;
while (skipped < 3 && mtl_skip_numeric_token(current))
++skipped;
if (skipped == 0)
return mtl_trim_value(original);
continue;
}
int option_args = -1;
if (mtl_token_equals(option_begin, option_end, "-mm"))
option_args = 2;
else if (mtl_token_equals(option_begin, option_end, "-bm") ||
mtl_token_equals(option_begin, option_end, "-boost") ||
mtl_token_equals(option_begin, option_end, "-texres") ||
mtl_token_equals(option_begin, option_end, "-clamp") ||
mtl_token_equals(option_begin, option_end, "-blendu") ||
mtl_token_equals(option_begin, option_end, "-blendv") ||
mtl_token_equals(option_begin, option_end, "-cc") ||
mtl_token_equals(option_begin, option_end, "-imfchan") ||
mtl_token_equals(option_begin, option_end, "-type"))
option_args = 1;
if (option_args < 0 || !mtl_skip_required_tokens(current, option_args))
return mtl_trim_value(original);
}
return mtl_trim_value(current);
}
static bool mtl_parseline(const char *line, MtlData &data)
{
if (*line == 0) return true;
@@ -394,13 +492,14 @@ static bool mtl_parseline(const char *line, MtlData &data)
ObjNewMtl new_mtl;
cur_mtl_name = line;
data.new_mtl_unmap[cur_mtl_name] = std::make_shared<ObjNewMtl>();
data.mtl_orders.emplace_back(cur_mtl_name);
break;
}
case 'm': {
if (*(line++) != 'a' || *(line++) != 'p' || *(line++) != '_' || *(line++) != 'K' || *(line++) != 'd') return false;
EATWS();
if (data.new_mtl_unmap.find(cur_mtl_name) != data.new_mtl_unmap.end()) {
data.new_mtl_unmap[cur_mtl_name]->map_Kd = line;
data.new_mtl_unmap[cur_mtl_name]->map_Kd = mtl_parse_texture_name(line);
}
break;
}
+3
View File
@@ -122,6 +122,9 @@ struct MtlData
// Version of the data structure for load / store in the private binary format.
int version;
std::unordered_map<std::string, std::shared_ptr<ObjNewMtl>> new_mtl_unmap;
// Material names in declaration order. new_mtl_unmap is unordered, but OBJ material
// indices are positional, so texture import needs the original order.
std::vector<std::string> mtl_orders;
};
extern bool objparse(const char *path, ObjData &data);
extern bool mtlparse(const char *path, MtlData &data);
+1 -1
View File
@@ -352,7 +352,7 @@ bool load_svg(const char *path, Model *model, std::string &message)
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
aPnt.Transform(aTrsf);
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
}
// BBS: copy triangles
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
+362 -9
View File
@@ -4195,6 +4195,8 @@ void GCode::export_layer_filaments(GCodeProcessorResult* result)
}
}
result->used_mixed_filaments = m_print->get_slice_used_mixed_filaments();
result->optimal_assignment.clear();
result->optimal_assignment.reserve(filament_map.size());
for (int nozzle_id : filament_map)
@@ -6004,9 +6006,16 @@ LayerResult GCode::process_layer(
const WipingExtrusions::ExtruderPerCopy *entity_overrides = nullptr;
if (! layer_tools.has_extruder(correct_extruder_id)) {
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
correct_extruder_id = layer_tools.extruders.back();
// A mixed-color slot is absent from layer_tools.extruders by design:
// resolve_mixed_filaments() replaced it with its physical components,
// and the sublayer block emits its geometry separately. Reassigning it
// to the last extruder here would print it in the wrong colour, so only
// fall back for genuinely stale (dontcare) extruders.
if (!layer_tools.is_mixed_slot(correct_extruder_id)) {
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
correct_extruder_id = layer_tools.extruders.back();
}
}
printing_extruders.clear();
if (is_anything_overridden && use_overrides) {
@@ -6094,7 +6103,16 @@ LayerResult GCode::process_layer(
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
single_object_instance_idx == size_t(-1) &&
print.config().print_order != PrintOrder::AsObjectList;
for (unsigned int filament_id : layer_tools.extruders) {
// A mixed-color slot is absent from layer_tools.extruders by design: resolve_mixed_filaments()
// replaced it with its physical components. Its geometry is still keyed under the slot in
// by_extruder though, and the sublayer emitter looks the plan up by slot id, so append the
// slots here. Appending rather than merging leaves the flush-optimized order untouched.
std::vector<unsigned int> plan_filaments = layer_tools.extruders;
for (const auto &grp : layer_tools.mixed_sub_layer_groups)
if (std::find(plan_filaments.begin(), plan_filaments.end(), grp.mixed_slot_0based) == plan_filaments.end())
plan_filaments.push_back(grp.mixed_slot_0based);
for (unsigned int filament_id : plan_filaments) {
auto objects_by_extruder_it = by_extruder.find(filament_id);
if (objects_by_extruder_it == by_extruder.end()) continue;
@@ -6275,8 +6293,22 @@ LayerResult GCode::process_layer(
}
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
std::vector<size_t> filament_instances_id;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
std::set<size_t> all_label_ids;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first)
all_label_ids.insert(instance.label_object_id);
// This extruder may also be printing sub-layers on behalf of a mixed slot, whose
// instances live under the slot id. Their labels belong in the same skip set, or
// exclude-object would not skip that geometry.
for (const auto &grp : layer_tools.mixed_sub_layer_groups)
for (unsigned int comp : grp.components_0based)
if (comp == extruder_id) {
auto mit = filament_to_print_instances.find(grp.mixed_slot_0based);
if (mit != filament_to_print_instances.end())
for (const InstanceToPrint &inst : mit->second.first)
all_label_ids.insert(inst.label_object_id);
break;
}
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
}
@@ -6557,6 +6589,318 @@ LayerResult GCode::process_layer(
}
}
}
// Mixed-color sublayer extrusion: if this extruder is a component of a mixed sublayer
// group, extrude the mixed slot's geometry at the appropriate sub-Z with scaled flow.
// Ported from BambuStudio and adapted to Orca's instance loop and its finer-grained
// per-role region filament options.
for (const auto &grp : layer_tools.mixed_sub_layer_groups) {
int sub_idx = -1;
for (size_t k = 0; k < grp.components_0based.size(); ++k) {
if (grp.components_0based[k] == extruder_id) {
sub_idx = static_cast<int>(k);
break;
}
}
if (sub_idx < 0)
continue;
auto mixed_instances_it = filament_to_print_instances.find(grp.mixed_slot_0based);
if (mixed_instances_it == filament_to_print_instances.end() || mixed_instances_it->second.first.empty())
continue;
double lh = grp.layer_height > 0. ? grp.layer_height : static_cast<double>(height);
double cumulative_h = 0.0;
for (int i = 0; i < sub_idx; ++i)
cumulative_h += grp.sub_heights[i];
double default_sub_h = grp.sub_heights[sub_idx];
double default_sub_z = print_z - lh + cumulative_h + default_sub_h;
m_sub_layer_flow_ratio = default_sub_h / lh;
m_sub_layer_height = default_sub_h;
m_nominal_z = default_sub_z;
gcode += this->set_extruder(extruder_id, default_sub_z);
for (InstanceToPrint &instance_to_print : mixed_instances_it->second.first) {
const bool use_per_volume = grp.is_gradient
&& !grp.per_volume_gradient.empty()
&& std::any_of(grp.per_volume_gradient.begin(), grp.per_volume_gradient.end(),
[&](const auto &kv) { return kv.first.obj == &instance_to_print.print_object; });
// --- Shared instance preamble (mirrors Orca's main instance loop) ---
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
const auto &inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
bool object_layer_over_raft = layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 &&
instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id();
m_config.apply(print.default_region_config());
m_config.apply(instance_to_print.print_object.config(), true);
m_layer = layer_to_print.layer();
m_object_layer_over_raft = object_layer_over_raft;
if (m_config.reduce_crossing_wall)
m_avoid_crossing_perimeters.init_layer(*m_layer);
if (this->config().gcode_label_objects) {
gcode += std::string("; printing object ") + instance_to_print.print_object.model_object()->name +
" id:" + std::to_string(instance_to_print.print_object.get_id()) + " copy " +
std::to_string(inst.id) + "\n";
}
if (m_enable_exclude_object) {
if (is_BBL_Printer()) {
m_writer.set_object_start_str(
std::string("; start printing object, unique label id: ") +
std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " +
_encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n");
} else {
const auto gflavor = print.config().gcode_flavor.value;
if (gflavor == gcfKlipper) {
m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") +
get_instance_name(&instance_to_print.print_object, inst.id) + "\n");
} else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) {
m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n");
}
}
}
m_extrusion_quality_estimator.set_current_object(&instance_to_print.print_object);
const Point &offset = inst.shift;
std::pair<const PrintObject*, Point> this_object_copy(&instance_to_print.print_object, offset);
if (m_last_obj_copy != this_object_copy)
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
// --- Build emission plan ---
// Each entry represents one travel_to_z + extrude pass. Per-object mode produces
// exactly 1 entry (all regions, single sub_z); per-volume mode produces N entries
// for tagged volumes plus an optional entry for untagged residue.
struct SubLayerEmitEntry {
double sub_h;
double sub_z;
std::function<bool(size_t region_idx)> region_filter;
bool skip = false;
};
std::vector<SubLayerEmitEntry> emit_plan;
auto compute_sub_zh = [&](double r1, double r2, double &out_sub_h, double &out_sub_z) {
std::vector<double> sub_heights_local(grp.components_0based.size());
for (size_t ci = 0; ci < grp.components_0based.size(); ++ci)
sub_heights_local[ci] = (static_cast<int>(ci) == grp.gradient_first_sorted_idx) ? r1 * lh : r2 * lh;
double cum = 0.0;
for (int ci = 0; ci < sub_idx; ++ci)
cum += sub_heights_local[ci];
out_sub_h = sub_heights_local[sub_idx];
out_sub_z = print_z - lh + cum + out_sub_h;
};
auto gradient_ratios = [](const auto &g) -> std::pair<double, double> {
double t = (g.total_layers > 0) ? (2.0 * g.current_idx + 1.0) / (2.0 * g.total_layers) : 0.5;
// Custom curve wins over linear range when present; OFF path stays bit-identical.
double r1 = g.curve.empty()
? (g.gradient_start + (g.gradient_end - g.gradient_start) * t)
: sample_gradient_curve(g.curve, t);
return {r1, 1.0 - r1};
};
// Orca splits BBS's three role filaments into five; a region belongs to the slot
// when any of its roles is assigned to it.
auto region_uses_slot = [](const PrintRegionConfig &rcfg, unsigned int slot_1b) {
return (unsigned int)rcfg.outer_wall_filament_id.value == slot_1b
|| (unsigned int)rcfg.inner_wall_filament_id.value == slot_1b
|| (unsigned int)rcfg.sparse_infill_filament_id.value == slot_1b
|| (unsigned int)rcfg.internal_solid_filament_id.value == slot_1b
|| (unsigned int)rcfg.top_surface_filament_id.value == slot_1b
|| (unsigned int)rcfg.bottom_surface_filament_id.value == slot_1b;
};
double obj_sub_z = default_sub_z;
if (use_per_volume) {
const PrintObject *po = &instance_to_print.print_object;
const unsigned int slot_1b = grp.mixed_slot_0based + 1;
// Discover tagged volumes and untagged presence for this instance.
std::set<ObjectID> tagged_volumes_present;
bool has_untagged_for_slot = false;
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
for (size_t r = 0; r < island.by_region.size(); ++r) {
const auto &region = island.by_region[r];
if (region.perimeters.empty() && region.infills.empty())
continue;
const PrintRegion &pr = print.get_print_region(r);
if (!region_uses_slot(pr.config(), slot_1b))
continue;
ObjectID vid = pr.gradient_volume_id();
if (vid.valid())
tagged_volumes_present.insert(vid);
else
has_untagged_for_slot = true;
}
}
// One entry per tagged volume.
for (const ObjectID &target_vid : tagged_volumes_present) {
auto vg_it = grp.per_volume_gradient.find({po, target_vid});
if (vg_it == grp.per_volume_gradient.end())
continue;
const auto &vg = vg_it->second;
auto [r1, r2] = gradient_ratios(vg);
bool vol_no_split = false;
bool skip_entry = false;
const size_t n = grp.components_0based.size();
if (n == 2 && vg.current_idx + 1 == vg.total_layers) {
const size_t dom_idx = (r1 >= r2) ? 0 : 1;
const unsigned int first_sorted_comp = grp.components_0based[grp.gradient_first_sorted_idx];
const unsigned int other_comp = grp.components_0based[1 - grp.gradient_first_sorted_idx];
const unsigned int dom_0b = (dom_idx == 0) ? first_sorted_comp : other_comp;
const unsigned int oth_0b = (dom_idx == 0) ? other_comp : first_sorted_comp;
if (dom_0b < oth_0b) {
vol_no_split = true;
if (extruder_id != dom_0b)
skip_entry = true;
}
}
double vol_sub_h = default_sub_h;
double vol_sub_z = default_sub_z;
if (vol_no_split) {
vol_sub_h = lh;
vol_sub_z = print_z;
} else {
compute_sub_zh(r1, r2, vol_sub_h, vol_sub_z);
}
emit_plan.push_back({vol_sub_h, vol_sub_z,
[target_vid, &print](size_t r) {
return print.get_print_region(r).gradient_volume_id() == target_vid;
},
skip_entry});
}
// Optional entry for untagged regions (modifier / painted / fuzzy_skin).
if (has_untagged_for_slot) {
double obj_sub_h = default_sub_h;
auto og_it = grp.per_object_gradient.find(po);
if (og_it != grp.per_object_gradient.end()) {
auto [r1, r2] = gradient_ratios(og_it->second);
compute_sub_zh(r1, r2, obj_sub_h, obj_sub_z);
}
emit_plan.push_back({obj_sub_h, obj_sub_z,
[&print](size_t r) {
return !print.get_print_region(r).gradient_volume_id().valid();
},
false});
}
} else {
// Legacy per-object path: single entry, no region filter.
double legacy_sub_h = default_sub_h;
obj_sub_z = default_sub_z;
if (grp.is_gradient) {
auto og_it = grp.per_object_gradient.find(&instance_to_print.print_object);
if (og_it != grp.per_object_gradient.end()) {
auto [r1, r2] = gradient_ratios(og_it->second);
compute_sub_zh(r1, r2, legacy_sub_h, obj_sub_z);
}
}
emit_plan.push_back({legacy_sub_h, obj_sub_z, nullptr, false});
}
// --- Unified emission loop ---
auto plan_has_infill = [](const std::vector<ObjectByExtruder::Island::Region> &by_region) {
for (const auto &r : by_region)
if (!r.infills.empty())
return true;
return false;
};
for (auto &entry : emit_plan) {
if (entry.skip)
continue;
m_sub_layer_flow_ratio = entry.sub_h / lh;
m_sub_layer_height = entry.sub_h;
m_nominal_z = entry.sub_z;
// Use the same lazy-Z mechanism as change_layer(): set the flag so travel_to
// fires even when m_last_pos coincides with the first extrusion point,
// ensuring Z reaches sub_z via the combined XY+Z move.
m_need_change_layer_lift_z = true;
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
const auto &src = island.by_region;
std::vector<ObjectByExtruder::Island::Region> subset_storage;
if (entry.region_filter) {
subset_storage.resize(src.size());
for (size_t r = 0; r < src.size(); ++r)
if (entry.region_filter(r))
subset_storage[r] = src[r];
}
const auto &by_region_specific = entry.region_filter ? subset_storage : src;
// Orca resolves infill-first per region inside extrude_perimeters()
// (unlike BBS, which branches on a single global flag), so mirror the
// main instance loop's ordering exactly.
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false);
if (!has_wipe_tower && need_insert_timelapse_gcode_for_traditional
&& printer_structure == PrinterStructure::psI3
&& !has_insert_timelapse_gcode && plan_has_infill(by_region_specific)) {
gcode += this->retract(false, false, auto_lift_type, true);
gcode += insert_timelapse_gcode();
has_insert_timelapse_gcode = true;
}
gcode += this->extrude_infill(print, by_region_specific, false);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
// ironing
gcode += this->extrude_infill(print, by_region_specific, true);
}
}
// --- Shared support ---
if (instance_to_print.object_by_extruder.support && !instance_to_print.object_by_extruder.support->empty()) {
if (use_per_volume) {
m_nominal_z = obj_sub_z;
m_need_change_layer_lift_z = true;
}
ExtrusionRole support_role = instance_to_print.object_by_extruder.support_extrusion_role;
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, support_role);
// Make sure ironing is the last (Orca names this role erIroning, not erSupportIroning).
if (support_role == erMixed || support_role == erSupportMaterialInterface)
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, erIroning);
}
// --- Shared instance footer (mirrors Orca's main instance loop) ---
if (!m_writer.is_object_start_str_empty()) {
m_writer.set_object_start_str("");
} else if (m_enable_exclude_object) {
if (is_BBL_Printer()) {
m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") +
std::to_string(instance_to_print.label_object_id) + "\n" +
"M625\n");
} else {
const auto gflavor = print.config().gcode_flavor.value;
if (gflavor == gcfKlipper) {
m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") +
get_instance_name(&instance_to_print.print_object, inst.id) + "\n");
} else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) {
m_writer.set_object_end_str(std::string("M486 S-1\n"));
}
}
}
}
m_sub_layer_flow_ratio = 0.0;
m_sub_layer_height = 0.0;
}
// Flush any pending object end label before leaving the sublayer block, otherwise the
// wipe tower's add_object_end_labels may consume it into a local temp string and the
// M625 would be lost for BBL printers.
if (!layer_tools.mixed_sub_layer_groups.empty()) {
m_writer.add_object_end_labels(gcode);
m_nominal_z = print_z;
m_need_change_layer_lift_z = true;
}
}
if (first_layer) {
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
@@ -7634,6 +7978,15 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
}
}
// Mixed-color sublayer: this path belongs to one sub-layer of a split layer, so scale the
// flow down to that sub-layer's share of the nominal layer height and report the sub-height
// as the effective extrusion height. Inert (ratio == 0) outside the sublayer emission block.
float effective_height = path.height;
if (m_sub_layer_flow_ratio > 0.0) {
_mm3_per_mm *= m_sub_layer_flow_ratio;
effective_height = static_cast<float>(m_sub_layer_height);
}
// Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio
// m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area)
double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm;
@@ -7933,8 +8286,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
gcode += buf;
}
if (last_was_wipe_tower || std::abs(m_last_height - path.height) > EPSILON) {
m_last_height = path.height;
if (last_was_wipe_tower || std::abs(m_last_height - effective_height) > EPSILON) {
m_last_height = effective_height;
sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), m_last_height);
gcode += buf;
}
@@ -8726,7 +9079,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp
continue;
Polygons temp;
temp.emplace_back(std::move(instance_bbox.polygon()));
temp.emplace_back(instance_bbox.polygon());
if (intersection_pl(travel, temp).empty())
continue;
+5
View File
@@ -747,6 +747,11 @@ private:
Print* m_curr_print = nullptr;
unsigned int m_toolchange_count;
coordf_t m_nominal_z;
// Mixed-color sublayer state. Non-zero only while emitting a mixed slot's sub-layer:
// scales extrusion flow to the sub-layer's share of the nominal layer height, and
// reports that sub-height as the effective extrusion height. Reset to 0 afterwards.
double m_sub_layer_flow_ratio = 0.0;
double m_sub_layer_height = 0.0;
bool m_need_change_layer_lift_z = false;
int m_start_gcode_filament = -1;
std::string m_filament_instances_code;
+1
View File
@@ -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();
+4
View File
@@ -306,6 +306,9 @@ class Print;
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
std::vector<unsigned int> nozzle_change_sequence;
std::vector<unsigned int> filament_change_sequence;
// 0-based mixed (virtual) filament slots actually used on this plate.
// Recorded before resolve_mixed_filaments expands them to physical components.
std::vector<unsigned int> used_mixed_filaments;
std::vector<int> optimal_assignment;
// first key stores `from` filament, second keys stores the `to` filament
std::map<std::pair<int,int>, int > filament_change_count_map;
@@ -357,6 +360,7 @@ class Print;
printer_extruder_id = other.printer_extruder_id;
layer_filaments = other.layer_filaments;
filament_change_sequence = other.filament_change_sequence;
used_mixed_filaments = other.used_mixed_filaments;
nozzle_change_sequence = other.nozzle_change_sequence;
optimal_assignment = other.optimal_assignment;
filament_change_count_map = other.filament_change_count_map;
+1 -1
View File
@@ -32,7 +32,7 @@ using ThumbnailsList = std::vector<ThumbnailData>;
struct ThumbnailsParams
{
const Vec2ds sizes;
const Vec2ds sizes{};
bool printable_only;
bool parts_only;
bool show_bed;
+803 -5
View File
@@ -7,6 +7,8 @@
#include "GCode/ToolOrderUtils.hpp"
#include "FilamentGroupUtils.hpp"
#include "MultiNozzleUtils.hpp"
#include "FilamentMixer.hpp"
#include "LocalesUtils.hpp"
#include "Utils.hpp"
#include "I18N.hpp"
@@ -22,8 +24,13 @@
#endif
#include <cassert>
#include <cstdio>
#include <limits>
#include <algorithm>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <unordered_map>
#include <libslic3r.h>
@@ -84,22 +91,28 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
}
// Return a zero based extruder from the region, or extruder_override if overriden.
// The region accessors below resolve mixed-color slots to the physical filament chosen for this
// layer by resolve_mixed_filaments(), because a virtual slot id is never a real tool. resolve_mixed()
// returns its argument unchanged for every filament that is not a mixed slot.
unsigned int LayerTools::wall_extruder_id(const PrintRegion &region) 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 &region) 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 &region) const
{
assert(region.config().internal_solid_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
unsigned int result = ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
return resolve_mixed(result);
}
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
@@ -135,7 +148,8 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c
} else
extruder = this->extruder_override;
return (extruder == 0) ? 0 : extruder - 1;
unsigned int result = (extruder == 0) ? 0 : extruder - 1;
return resolve_mixed(result);
}
static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height)
@@ -402,7 +416,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
// so we shouldn't reorder first layer in reorder function
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
this->resolve_mixed_filaments(print.config());
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
this->enforce_mixed_component_order();
m_sorted = true;
double max_layer_height = 0.;
@@ -422,6 +438,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
if (this->insert_wipe_tower_extruder()) {
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
// Orca reorders a second time here (BBS has no such path); re-enforce so the
// mixed sub-layer component order survives the extra pass.
this->enforce_mixed_component_order();
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
}
@@ -433,7 +452,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
// so we shouldn't reorder first layer in reorder function
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
this->resolve_mixed_filaments(object.print()->config());
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
this->enforce_mixed_component_order();
m_sorted = true;
double max_layer_height = calc_max_layer_height(object.print()->config(), object.config().layer_height);
@@ -441,6 +462,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
if (this->insert_wipe_tower_extruder()) {
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
// Orca reorders a second time here (BBS has no such path); re-enforce so the
// mixed sub-layer component order survives the extra pass.
this->enforce_mixed_component_order();
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
}
@@ -723,6 +747,38 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
it_per_layer_extruder_override = per_layer_extruder_switches.begin();
unsigned int extruder_override = 0;
// Pre-compute 1-based IDs of mixed filament slots for per-object tracking.
// mixed_slots_1based covers ALL mixed slots (needed by calc_slot_lh for
// accurate layer height when a slot skips layers). gradient_slots_1based
// and per_part_slots_1based are subsets for gradient-specific logic.
std::set<unsigned int> mixed_slots_1based;
std::set<unsigned int> gradient_slots_1based;
std::set<unsigned int> per_part_slots_1based;
{
const PrintConfig &cfg = object.print()->config();
const auto &is_mixed = cfg.filament_is_mixed.values;
const auto &grad_flags = cfg.filament_mixed_gradient.values;
const auto &per_part_flags = cfg.filament_mixed_gradient_per_part.values;
const auto &comp_strs = cfg.filament_mixed_components.values;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
auto comps = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
if (comps.size() < 2)
continue;
mixed_slots_1based.insert(static_cast<unsigned int>(i + 1));
// Gradient/per-part are only defined for 2-component slots; keep their
// tracking limited to them (mirrors the is_gradient guard at resolve time).
if (comps.size() != 2)
continue;
if (i >= grad_flags.size() || !grad_flags[i])
continue;
gradient_slots_1based.insert(static_cast<unsigned int>(i + 1));
if (i < per_part_flags.size() && per_part_flags[i])
per_part_slots_1based.insert(static_cast<unsigned int>(i + 1));
}
}
// BBS: collect first layer extruders of an object's wall, which will be used by brim generator
int layerCount = 0;
std::vector<int> firstLayerExtruders;
@@ -732,6 +788,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
for (auto layer : object.layers()) {
LayerTools &layer_tools = this->tools_for_layer(layer->print_z);
m_object_all_layer_indices[&object].push_back(
static_cast<size_t>(&layer_tools - m_layer_tools.data()));
// Override extruder with the next
for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override)
extruder_override = (int)it_per_layer_extruder_override->second;
@@ -739,6 +798,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
// Store the current extruder override (set to zero if no overriden), so that layer_tools.wiping_extrusions().is_overridable_and_mark() will use it.
layer_tools.extruder_override = extruder_override;
// Snapshot extruders before this object's regions to track new additions.
const size_t ext_snapshot = layer_tools.extruders.size();
// What extruders are required to print this object layer?
for (const LayerRegion *layerm : layer->regions()) {
const PrintRegion &region = layerm->region();
@@ -805,6 +867,54 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill)
layer_tools.has_object = true;
}
// Record mixed slot usage for this object at this layer.
// All mixed slots are tracked (not just gradient) so that calc_slot_lh
// can compute accurate layer heights even when a slot skips layers.
if (!mixed_slots_1based.empty()) {
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
std::set<unsigned int> seen;
for (size_t ei = ext_snapshot; ei < layer_tools.extruders.size(); ++ei) {
unsigned int ext_1based = layer_tools.extruders[ei];
if (mixed_slots_1based.count(ext_1based) && seen.insert(ext_1based).second)
m_mixed_object_layers[ext_1based - 1][&object].push_back(layer_idx);
}
}
// Per-part gradient: walk LayerRegions and record which (slot, ModelVolume) pairs
// contributed to this layer. Only regions tagged by PrintApply.cpp's get_create_region
// (i.e. gradient_volume_id().valid()) are considered, so this loop is a strict no-op
// unless per_part_gradient is enabled for at least one slot AND the corresponding
// ModelObject has >=2 model-part volumes using that slot. The per-object pass above is
// unaffected — both run the same layer's data through orthogonal containers.
if (!per_part_slots_1based.empty()) {
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
std::set<std::pair<unsigned int, ObjectID>> vol_seen;
for (const LayerRegion *layerm : layer->regions()) {
if (layerm->slices.empty())
continue;
const PrintRegion &region = layerm->region();
ObjectID vol_id = region.gradient_volume_id();
if (! vol_id.valid())
continue;
const PrintRegionConfig &rcfg = region.config();
// Orca splits BBS's three role slots into five; cover them all so a mixed
// slot used by any role is tracked.
const unsigned int role_slots[5] = {
static_cast<unsigned int>(rcfg.outer_wall_filament_id.value),
static_cast<unsigned int>(rcfg.inner_wall_filament_id.value),
static_cast<unsigned int>(rcfg.sparse_infill_filament_id.value),
static_cast<unsigned int>(rcfg.top_surface_filament_id.value),
static_cast<unsigned int>(rcfg.bottom_surface_filament_id.value),
};
for (unsigned int ext_1based : role_slots) {
if (ext_1based >= 1
&& per_part_slots_1based.count(ext_1based)
&& vol_seen.insert({ext_1based, vol_id}).second)
m_gradient_volume_layers[ext_1based - 1][{&object, vol_id}].push_back(layer_idx);
}
}
}
layerCount++;
}
@@ -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 &lt : 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 &lt = 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 &lt = m_layer_tools[i];
LayerTools &lt_next = m_layer_tools[i + 1];
if (!lt.has_wipe_tower || !lt_next.has_wipe_tower) {
++i;
continue;
}
coordf_t gap = lt_next.print_z - lt.print_z;
if (gap <= max_layer_height + EPSILON) {
++i;
continue;
}
LayerTools lt_new(0.5 * (lt.print_z + lt_next.print_z));
lt_new.has_wipe_tower = true;
if (!lt_next.extruders.empty())
lt_new.extruders.push_back(lt_next.extruders.front());
else if (!lt.extruders.empty())
lt_new.extruders.push_back(lt.extruders.back());
lt_new.wipe_tower_partitions = lt_next.wipe_tower_partitions;
m_layer_tools.insert(m_layer_tools.begin() + i + 1, lt_new);
}
}
// If the model contains empty layers (such as https://github.com/prusa3d/Slic3r/issues/1266), there might be layers
// that were not marked as has_wipe_tower, even when they should have been. This produces a crash with soluble supports
// and maybe other problems. We will therefore go through layer_tools and detect and fix this.
@@ -1945,6 +2133,605 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_
return result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult();
}
static double snap_to_simple_fraction(double r, int max_denom = 10)
{
double best_r = r;
double best_err = 1.0;
for (int q = 1; q <= max_denom; ++q) {
int p = (int)std::round(r * q);
if (p < 0) p = 0;
if (p > q) p = q;
double candidate = (double)p / q;
double err = std::abs(candidate - r);
if (err < best_err) {
best_err = err;
best_r = candidate;
}
}
return best_r;
}
void ToolOrdering::resolve_mixed_filaments(const PrintConfig &config)
{
const auto &is_mixed = config.filament_is_mixed.values;
const auto &comp_strs = config.filament_mixed_components.values;
const auto &ratio_strs = config.filament_mixed_sublayer_ratios.values;
// Capture mixed slots that actually appear on layers before they are expanded to
// physical components. Assigned-but-unused mixed slots never enter layer_tools.
m_used_mixed_filaments.clear();
if (has_any_mixed_filament(is_mixed)) {
std::set<unsigned int> used;
for (const LayerTools &lt : m_layer_tools)
for (unsigned int ext : lt.extruders)
if (ext < is_mixed.size() && is_mixed[ext])
used.insert(ext);
m_used_mixed_filaments.assign(used.begin(), used.end());
}
if (!has_any_mixed_filament(is_mixed))
return;
const bool sublayer_enabled = config.enable_mixed_color_sublayer.value;
struct SlotInfo {
std::vector<unsigned int> components; // 1-based
std::vector<double> ratios;
std::vector<long long> accum; // deficit accumulator (integer, unit: 1e-6 mm)
};
std::vector<SlotInfo> slots(is_mixed.size());
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
slots[i].components = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
if (slots[i].components.size() < 2) {
slots[i].components.clear();
continue;
}
for (unsigned int cid : slots[i].components) {
unsigned int idx0 = cid - 1;
if (idx0 >= is_mixed.size() || (idx0 < is_mixed.size() && is_mixed[idx0])) {
slots[i].components.clear();
break;
}
}
if (slots[i].components.empty())
continue;
slots[i].ratios = parse_mixed_ratios(
i < ratio_strs.size() ? ratio_strs[i] : "", slots[i].components.size());
if (!sublayer_enabled) {
for (double &r : slots[i].ratios)
r = snap_to_simple_fraction(r);
double sum = 0;
for (double r : slots[i].ratios) sum += r;
if (sum > 0)
for (double &r : slots[i].ratios) r /= sum;
}
slots[i].accum.assign(slots[i].components.size(), 0LL);
}
// Parse gradient settings per slot
const auto &gradient_flags = config.filament_mixed_gradient.values;
const auto &gradient_range_strs = config.filament_mixed_gradient_range.values;
const auto &gradient_curve_strs = config.filament_mixed_gradient_curve.values;
struct GradientInfo {
double start = 0.10;
double end_val = 0.90;
GradientCurve curve; // empty -> use linear (start, end_val); non-empty wins
};
std::vector<bool> is_gradient(is_mixed.size(), false);
std::vector<GradientInfo> gradient_info(is_mixed.size());
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i] || slots[i].components.size() != 2)
continue;
if (i >= gradient_flags.size() || !gradient_flags[i])
continue;
is_gradient[i] = true;
if (i < gradient_range_strs.size() && !gradient_range_strs[i].empty()) {
CNumericLocalesSetter c_locale_setter;
float v0 = 0, v1 = 0;
if (std::sscanf(gradient_range_strs[i].c_str(), "%f,%f", &v0, &v1) == 2 &&
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
gradient_info[i].start = v0;
gradient_info[i].end_val = v1;
}
}
if (i < gradient_curve_strs.size() && !gradient_curve_strs[i].empty())
gradient_info[i].curve = parse_gradient_curve(gradient_curve_strs[i]);
}
// Pass 1: identify continuous runs for each gradient slot (Per-Run).
// A "run" is a maximal sequence of consecutive layers where the slot appears.
struct GradientRunInfo {
std::vector<size_t> run_lengths;
int current_run = -1;
size_t current_idx = 0;
bool prev_appeared = false;
bool last_absent_was_relevant = false;
};
std::map<unsigned int, GradientRunInfo> gradient_runs;
for (size_t i = 0; i < is_mixed.size(); ++i)
if (is_gradient[i]) gradient_runs[static_cast<unsigned int>(i)] = {};
// Build per-slot sets of all layer indices where any slot-owning object has a
// layer. Used by gradient run detection (a gap is real only if the slot is
// absent at a layer belonging to one of its own objects) and by calc_slot_lh
// to keep prev_relevant_z_for_slot current even when a slot skips many layers.
std::map<unsigned int, std::set<size_t>> slot_relevant_layers;
for (auto &[slot_idx, obj_map] : m_mixed_object_layers) {
for (auto &[obj, _] : obj_map) {
auto it = m_object_all_layer_indices.find(obj);
if (it != m_object_all_layer_indices.end())
slot_relevant_layers[slot_idx].insert(it->second.begin(), it->second.end());
}
}
if (!gradient_runs.empty()) {
for (size_t li = 0; li < m_layer_tools.size(); ++li) {
if (li == 0) continue;
const auto &lt = m_layer_tools[li];
for (auto &[slot, run] : gradient_runs) {
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
if (here) {
bool real_gap = false;
if (!run.prev_appeared && !run.run_lengths.empty()) {
real_gap = run.last_absent_was_relevant;
}
if (run.run_lengths.empty() || real_gap)
run.run_lengths.push_back(0);
run.run_lengths.back()++;
run.last_absent_was_relevant = false;
} else if (!run.run_lengths.empty()) {
auto rel_it = slot_relevant_layers.find(slot);
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(li))
run.last_absent_was_relevant = true;
}
run.prev_appeared = here;
}
}
for (auto &[slot, run] : gradient_runs) {
run.current_run = -1;
run.current_idx = 0;
run.prev_appeared = false;
run.last_absent_was_relevant = false;
}
}
// Per-object gradient: pre-compute per-object runs (respecting Z gaps within each object).
struct PerObjRunState {
std::vector<size_t> run_start_offsets; // index into layer_indices where each run starts
std::vector<size_t> run_lengths;
int current_run = -1;
size_t current_idx = 0;
};
// Detect whether a gap between two consecutive gradient-slot appearances is a
// real run break. A gap is real only if the object has its own layer inside the
// gap that does NOT use the gradient slot (i.e. the slot was genuinely absent).
// Uses lower_bound to skip global indices that don't belong to the object.
auto has_real_gap = [](size_t prev_idx, size_t cur_idx,
const std::set<size_t>& obj_set,
const std::set<size_t>& slot_set) -> bool {
for (auto it = obj_set.lower_bound(prev_idx + 1);
it != obj_set.end() && *it < cur_idx; ++it) {
if (!slot_set.count(*it))
return true;
}
return false;
};
// Segment a sorted list of layer indices into runs, using has_real_gap to decide
// where to break. Shared by the per-object and per-volume paths below.
auto segment_runs = [&](const std::vector<size_t>& layer_indices,
const std::set<size_t>& obj_set,
const std::set<size_t>& slot_set) -> PerObjRunState {
PerObjRunState st;
for (size_t i = 0; i < layer_indices.size(); ++i) {
bool new_run = (i == 0) ||
has_real_gap(layer_indices[i - 1], layer_indices[i], obj_set, slot_set);
if (new_run) {
st.run_start_offsets.push_back(i);
st.run_lengths.push_back(0);
}
st.run_lengths.back()++;
}
return st;
};
std::map<unsigned int, std::map<const PrintObject*, PerObjRunState>> per_obj_runs;
for (auto &[slot, obj_map] : m_mixed_object_layers) {
if (slot >= is_gradient.size() || !is_gradient[slot])
continue;
for (auto &[obj, layer_indices] : obj_map) {
sort_remove_duplicates(layer_indices);
// Erase layer 0 — this mutation is also relied upon by the Pass 2 binary_search below.
if (!layer_indices.empty() && layer_indices.front() == 0)
layer_indices.erase(layer_indices.begin());
const auto &all_obj_layers = m_object_all_layer_indices[obj];
std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end());
std::set<size_t> grad_set(layer_indices.begin(), layer_indices.end());
per_obj_runs[slot][obj] = segment_runs(layer_indices, all_obj_set, grad_set);
}
}
// Per-volume gradient: mirror the per-object run-segmentation logic above for
// m_gradient_volume_layers. When per_part_gradient is off (or no qualifying volume exists),
// m_gradient_volume_layers is empty and per_vol_runs ends up empty too — so all subsequent
// checks of `per_vol_runs.find(slot) != end()` will fail and the legacy per-object path
// remains the only path taken.
using VolumeKey = LayerTools::MixedSubLayerGroup::VolumeKey;
std::map<unsigned int, std::map<VolumeKey, PerObjRunState>> per_vol_runs;
for (auto &[slot, vol_map] : m_gradient_volume_layers) {
if (slot >= is_gradient.size() || !is_gradient[slot])
continue;
for (auto &[vkey, layer_indices] : vol_map) {
sort_remove_duplicates(layer_indices);
if (!layer_indices.empty() && layer_indices.front() == 0)
layer_indices.erase(layer_indices.begin());
const auto &all_obj_layers = m_object_all_layer_indices[vkey.obj];
std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end());
std::set<size_t> vol_grad_set(layer_indices.begin(), layer_indices.end());
per_vol_runs[slot][vkey] = segment_runs(layer_indices, all_obj_set, vol_grad_set);
}
}
// Pass 2: resolve per layer
coordf_t prev_print_z = 0.;
// Track last print_z per mixed slot so that layer height is computed from the
// slot's own previous appearance, not from a global Z that may include layers
// belonging only to other objects with different layer heights.
std::map<unsigned int, coordf_t> prev_print_z_for_slot;
// Track the last Z where a slot-owning object had ANY layer (regardless of
// whether the slot was present). Used to detect genuine gaps: if the slot was
// absent but its owner objects had layers, prev_relevant_z advances while
// prev_print_z_for_slot stays stale. Taking the max of both gives correct lh.
std::map<unsigned int, coordf_t> prev_relevant_z_for_slot;
// Compute the effective layer height for a mixed slot by choosing the best
// reference Z among: (1) the slot's own last Z, (2) the last Z where the
// slot's owning object had any layer, (3) the global previous Z as fallback
// when the slot appears for the first time.
auto calc_slot_lh = [&](unsigned int ext, coordf_t print_z) -> double {
auto slot_pz_it = prev_print_z_for_slot.find(ext);
auto rel_pz_it = prev_relevant_z_for_slot.find(ext);
coordf_t base_z = prev_print_z;
if (slot_pz_it != prev_print_z_for_slot.end()) {
base_z = slot_pz_it->second;
if (rel_pz_it != prev_relevant_z_for_slot.end())
base_z = std::max(base_z, rel_pz_it->second);
}
double lh = print_z - base_z;
return (lh > 0.) ? lh : 0.2; // 0.2mm safety fallback; should not trigger in normal operation
};
for (LayerTools &lt : m_layer_tools) {
size_t layer_idx = static_cast<size_t>(&lt - m_layer_tools.data());
// Update gradient run state (skip first layer to match counting).
if (layer_idx > 0) {
for (auto &[slot, run] : gradient_runs) {
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
if (here) {
if (!run.prev_appeared) {
if (run.last_absent_was_relevant || run.current_run < 0) {
run.current_run++;
run.current_idx = 0;
}
}
run.last_absent_was_relevant = false;
} else {
auto rel_it = slot_relevant_layers.find(slot);
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(layer_idx))
run.last_absent_was_relevant = true;
}
run.prev_appeared = here;
}
}
std::vector<unsigned int> new_extruders;
for (unsigned int ext : lt.extruders) {
if (ext >= slots.size() || slots[ext].components.empty()) {
new_extruders.push_back(ext);
continue;
}
auto &s = slots[ext];
// Skip sublayer splitting for the first layer to preserve bed adhesion.
if (sublayer_enabled && layer_idx > 0) {
double lh = calc_slot_lh(ext, lt.print_z);
size_t n = s.components.size();
std::vector<double> sub_heights;
bool gradient_last_no_split = false;
unsigned int gradient_last_dominant_0b = 0;
if (is_gradient[ext] && n == 2) {
auto gr_it = gradient_runs.find(ext);
if (gr_it != gradient_runs.end() && gr_it->second.current_run >= 0 &&
static_cast<size_t>(gr_it->second.current_run) < gr_it->second.run_lengths.size()) {
auto &run = gr_it->second;
size_t N = run.run_lengths[run.current_run];
size_t idx = run.current_idx++;
double t = (N > 0) ? (2.0 * idx + 1.0) / (2.0 * N) : 0.5;
// Custom curve wins over linear range when present; OFF path stays bit-identical.
double r1 = gradient_info[ext].curve.empty()
? (gradient_info[ext].start + (gradient_info[ext].end_val - gradient_info[ext].start) * t)
: sample_gradient_curve(gradient_info[ext].curve, t);
double r2 = 1.0 - r1;
sub_heights.push_back(r1 * lh);
sub_heights.push_back(r2 * lh);
// The sublayer split path sorts components by physical ID ascending;
// the higher-ID component ends up on top (visible surface). If the
// gradient's dominant component has the lower physical ID, splitting
// would put the non-dominant color on the visible top surface. In
// that case, skip the split and print this final run-layer as pure
// dominant color to preserve the gradient appearance.
if (idx == N - 1) {
// When r1 == r2 (exactly 50/50), component[0] is treated as dominant.
size_t dominant = (r1 >= r2) ? 0 : 1;
unsigned int dom_0b = s.components[dominant] - 1;
unsigned int oth_0b = s.components[1 - dominant] - 1;
if (dom_0b < oth_0b) {
gradient_last_no_split = true;
gradient_last_dominant_0b = dom_0b;
}
}
} else {
for (double r : s.ratios)
sub_heights.push_back(r * lh);
}
} else {
for (double r : s.ratios)
sub_heights.push_back(r * lh);
}
// Per-part gradient: when this slot has any qualifying volume, the global
// no-split short-circuit must NOT bypass MixedSubLayerGroup creation — each
// volume needs its own no-split decision in GCode.cpp (a per-volume "last
// run-layer" can occur on a different layer index than the per-object one). We
// still keep the per-object short-circuit when per_vol_runs[ext] is empty, which
// covers the legacy path bit-identically.
bool per_vol_active_for_slot = per_vol_runs.find(ext) != per_vol_runs.end()
&& !per_vol_runs[ext].empty();
if (gradient_last_no_split && !per_vol_active_for_slot) {
lt.mixed_filament_resolution[ext] = gradient_last_dominant_0b;
new_extruders.push_back(gradient_last_dominant_0b);
prev_print_z_for_slot[ext] = lt.print_z;
continue;
}
LayerTools::MixedSubLayerGroup grp;
grp.mixed_slot_0based = ext;
grp.layer_height = lh;
grp.is_gradient = is_gradient[ext];
for (size_t k = 0; k < s.components.size(); ++k) {
unsigned int comp_0based = s.components[k] - 1;
grp.components_0based.push_back(comp_0based);
}
grp.sub_heights = sub_heights;
// Write gradient metadata (run-aware). Both per_object_gradient and
// per_volume_gradient are populated independently from their own run-state
// machines; the GCode emitter chooses per-region:
// - tagged region (gradient_volume_id valid) -> per_volume_gradient[{obj, vol}]
// - untagged region (modifier / painted / etc.) -> per_object_gradient[obj]
// Populating both keeps the per-object run state correct even when per-volume
// takes over for the same (slot, obj), and lets untagged geometry (which is
// never split per-volume) keep its per-object gradient ratios.
if (grp.is_gradient) {
auto vol_runs_slot_it = per_vol_runs.find(ext);
if (vol_runs_slot_it != per_vol_runs.end()) {
auto vol_slot_it = m_gradient_volume_layers.find(ext);
for (auto &[vkey, st] : vol_runs_slot_it->second) {
auto &layer_indices = vol_slot_it->second[vkey];
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
continue;
if (st.current_run < 0 ||
st.current_idx >= st.run_lengths[st.current_run]) {
st.current_run++;
st.current_idx = 0;
}
size_t run_N = st.run_lengths[st.current_run];
size_t run_idx = st.current_idx++;
grp.per_volume_gradient[vkey] = {
run_N,
run_idx,
gradient_info[ext].start,
gradient_info[ext].end_val,
gradient_info[ext].curve,
};
}
}
auto runs_slot_it = per_obj_runs.find(ext);
if (runs_slot_it != per_obj_runs.end()) {
auto slot_it = m_mixed_object_layers.find(ext);
for (auto &[obj, st] : runs_slot_it->second) {
auto &layer_indices = slot_it->second[obj];
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
continue;
if (st.current_run < 0 ||
st.current_idx >= st.run_lengths[st.current_run]) {
st.current_run++;
st.current_idx = 0;
}
size_t run_N = st.run_lengths[st.current_run];
size_t run_idx = st.current_idx++;
grp.per_object_gradient[obj] = {
run_N,
run_idx,
gradient_info[ext].start,
gradient_info[ext].end_val,
gradient_info[ext].curve,
};
}
}
}
if (grp.components_0based.size() > 1) {
unsigned int first_comp_0based = s.components[0] - 1;
std::vector<size_t> idx(grp.components_0based.size());
std::iota(idx.begin(), idx.end(), 0);
std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b) {
return grp.components_0based[a] < grp.components_0based[b];
});
std::vector<unsigned int> sorted_comps;
std::vector<double> sorted_heights;
for (size_t i : idx) {
sorted_comps.push_back(grp.components_0based[i]);
sorted_heights.push_back(grp.sub_heights[i]);
}
grp.components_0based = std::move(sorted_comps);
grp.sub_heights = std::move(sorted_heights);
if (grp.is_gradient) {
for (size_t i = 0; i < grp.components_0based.size(); ++i) {
if (grp.components_0based[i] == first_comp_0based) {
grp.gradient_first_sorted_idx = static_cast<int>(i);
break;
}
}
}
}
for (unsigned int comp : grp.components_0based)
new_extruders.push_back(comp);
lt.mixed_sub_layer_groups.push_back(std::move(grp));
prev_print_z_for_slot[ext] = lt.print_z;
} else {
// Deficit Round-Robin: pick one component per layer.
// Weight by layer height so volume ratios stay accurate
// even with adaptive layer heights.
double lh = calc_slot_lh(ext, lt.print_z);
long long lh_i = std::llround(lh * 1e6);
// For 2-component gradient on the first layer, use the gradient's
// starting ratio instead of the configured mixing ratio so the
// selected filament matches the gradient's "from" end.
// Only affects the first layer; when sublayer splitting is enabled
// (required for gradient), layers 1+ take the sublayer path and
// do not touch the DRR accumulator.
if (layer_idx == 0 && is_gradient[ext] && s.components.size() == 2) {
double r0 = gradient_info[ext].start;
s.accum[0] += std::llround(r0 * lh_i);
s.accum[1] += std::llround((1.0 - r0) * lh_i);
} else {
for (size_t k = 0; k < s.ratios.size(); ++k)
s.accum[k] += std::llround(s.ratios[k] * lh_i);
}
size_t sel = 0;
for (size_t k = 1; k < s.accum.size(); ++k)
if (s.accum[k] > s.accum[sel])
sel = k;
s.accum[sel] -= lh_i;
unsigned int resolved = s.components[sel] - 1;
lt.mixed_filament_resolution[ext] = resolved;
new_extruders.push_back(resolved);
prev_print_z_for_slot[ext] = lt.print_z;
}
}
lt.extruders = new_extruders;
sort_remove_duplicates(lt.extruders);
// Update prev_relevant_z: for each slot that has relevant-layer tracking,
// advance if the current layer belongs to a slot-owning object.
for (auto &[slot, rel_set] : slot_relevant_layers) {
if (rel_set.count(layer_idx))
prev_relevant_z_for_slot[slot] = lt.print_z;
}
prev_print_z = lt.print_z;
}
}
void ToolOrdering::enforce_mixed_component_order()
{
for (LayerTools &lt : m_layer_tools) {
if (lt.mixed_sub_layer_groups.empty())
continue;
// Build a set of extruders present in lt.extruders for fast lookup.
std::set<unsigned int> ext_set(lt.extruders.begin(), lt.extruders.end());
// 1. Build DAG from mixed group constraints.
// For each group [c0, c1, c2, ...], add edges c0->c1, c1->c2, ...
// Only between components that are both present in lt.extruders.
// Use an edge set to avoid duplicate edges inflating in-degree.
std::map<unsigned int, std::vector<unsigned int>> adj;
std::map<unsigned int, int> in_degree;
std::set<std::pair<unsigned int, unsigned int>> edge_set;
for (unsigned int ext : lt.extruders)
in_degree[ext] = 0;
for (const auto &grp : lt.mixed_sub_layer_groups) {
for (size_t i = 0; i + 1 < grp.components_0based.size(); ++i) {
unsigned int a = grp.components_0based[i];
unsigned int b = grp.components_0based[i + 1];
if (!ext_set.count(a) || !ext_set.count(b))
continue;
if (edge_set.insert({a, b}).second) {
adj[a].push_back(b);
in_degree[b] += 1;
}
}
}
// 2. Record original position (from flush optimizer) as priority.
std::map<unsigned int, size_t> orig_pos;
for (size_t i = 0; i < lt.extruders.size(); ++i)
orig_pos[lt.extruders[i]] = i;
// 3. Kahn's topological sort with priority queue (prefer original position).
auto cmp = [&orig_pos](unsigned int lhs, unsigned int rhs) {
return orig_pos[lhs] > orig_pos[rhs]; // min-heap by orig_pos
};
std::priority_queue<unsigned int, std::vector<unsigned int>, decltype(cmp)> pq(cmp);
for (unsigned int ext : lt.extruders) {
if (in_degree[ext] == 0)
pq.push(ext);
}
std::vector<unsigned int> ordered;
ordered.reserve(lt.extruders.size());
while (!pq.empty()) {
unsigned int ext = pq.top();
pq.pop();
ordered.push_back(ext);
if (auto it = adj.find(ext); it != adj.end()) {
for (unsigned int next : it->second) {
if (--in_degree[next] == 0)
pq.push(next);
}
}
}
// Safety: if topological sort didn't produce all elements, keep original order.
if (ordered.size() != lt.extruders.size())
ordered = lt.extruders;
// 4. Verify: every mixed group's component order is preserved as subsequence.
for (const auto &grp : lt.mixed_sub_layer_groups) {
size_t prev_pos = 0;
bool valid = true;
for (unsigned int c : grp.components_0based) {
if (!ext_set.count(c))
continue;
auto it = std::find(ordered.begin() + prev_pos, ordered.end(), c);
if (it == ordered.end()) { valid = false; break; }
prev_pos = (it - ordered.begin()) + 1;
}
assert(valid && "enforce_mixed_component_order: mixed group subsequence violated");
(void)valid;
}
lt.extruders = ordered;
}
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -1998,6 +2785,17 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
std::vector<unsigned int> used_filaments = collect_sorted_used_filaments(layer_filaments);
std::vector<std::set<int>>geometric_unprintables = m_print->get_geometric_unprintable_filaments();
// Unprintable sets are keyed by filament id, but a mixed-color slot is virtual: what actually
// reaches the nozzle are its components. Expand the slot to those components so a geometric
// restriction is applied to the filaments really being printed. No-op without mixed filaments.
{
const auto &is_mixed = m_print->config().filament_is_mixed.values;
const auto &comp_strs = m_print->config().filament_mixed_components.values;
if (has_any_mixed_filament(is_mixed))
expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs);
}
std::vector<std::set<int>>physical_unprintables = m_print->get_physical_unprintable_filaments(used_filaments);
auto filament_unprintable_volumes = m_print->get_filament_unprintable_flow(used_filaments);
+86
View File
@@ -5,12 +5,16 @@
#include "../libslic3r.h"
#include <functional>
#include <map>
#include <utility>
#include <boost/container/small_vector.hpp>
#include "../FilamentGroup.hpp"
#include "../FilamentMixer.hpp"
#include "../MultiNozzleUtils.hpp"
#include "../ExtrusionEntity.hpp"
#include "../ObjectID.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
@@ -172,6 +176,65 @@ public:
// Custom G-code (color change, extruder switch, pause) to be performed before this layer starts to print.
const CustomGCode::Item *custom_gcode = nullptr;
// 0-based mixed filament slot → 0-based resolved physical filament for this layer.
// Populated by ToolOrdering::resolve_mixed_filaments(). Empty when no mixed filaments.
std::map<unsigned int, unsigned int> mixed_filament_resolution;
unsigned int resolve_mixed(unsigned int filament_0based) const {
auto it = mixed_filament_resolution.find(filament_0based);
return (it != mixed_filament_resolution.end()) ? it->second : filament_0based;
}
struct MixedSubLayerGroup {
unsigned int mixed_slot_0based;
std::vector<unsigned int> components_0based;
std::vector<double> sub_heights; // per-component, sum ≈ layer_height
double layer_height = 0.; // the actual lh used to compute sub_heights
bool is_gradient = false;
int gradient_first_sorted_idx = 0; // index of "first" config component after sorting
struct ObjectGradient {
size_t total_layers;
size_t current_idx;
double gradient_start;
double gradient_end;
GradientCurve curve; // empty -> linear fallback (start, end); non-empty wins
};
std::map<const PrintObject*, ObjectGradient> per_object_gradient;
// Per-volume gradient: same metadata layout as ObjectGradient but keyed by
// (PrintObject*, ModelVolume id). Populated only when filament_mixed_gradient_per_part is
// enabled for this slot AND the corresponding ModelObject contains >=2 model-part volumes
// using this slot. When non-empty for a given (PrintObject*), GCode emission takes the
// per-volume path for tagged regions; untagged regions (modifier/painted/fuzzy_skin) still
// use per_object_gradient. Both maps are populated in parallel to keep run states correct.
struct VolumeKey {
const PrintObject* obj;
ObjectID volume_id;
bool operator<(const VolumeKey &o) const {
if (obj != o.obj) return std::less<const PrintObject*>{}(obj, o.obj);
return volume_id < o.volume_id;
}
bool operator==(const VolumeKey &o) const {
return obj == o.obj && volume_id == o.volume_id;
}
};
using VolumeGradient = ObjectGradient;
std::map<VolumeKey, VolumeGradient> per_volume_gradient;
};
std::vector<MixedSubLayerGroup> mixed_sub_layer_groups;
const MixedSubLayerGroup* mixed_group_by_slot(unsigned int slot_id) const {
for (const auto &g : mixed_sub_layer_groups)
if (g.mixed_slot_0based == slot_id)
return &g;
return nullptr;
}
bool is_mixed_slot(unsigned int slot_id) const {
return mixed_group_by_slot(slot_id) != nullptr;
}
WipingExtrusions& wiping_extrusions() {
m_wiping_extrusions.set_layer_tools_ptr(this);
return m_wiping_extrusions;
@@ -227,6 +290,9 @@ public:
// For a multi-material print, the printing extruders are ordered in the order they shall be primed.
const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; }
// 0-based mixed (virtual) slots that appeared on layers before resolve_mixed_filaments
// expanded them to physical components.
const std::vector<unsigned int>& used_mixed_filaments() const { return m_used_mixed_filaments; }
// Find LayerTools with the closest print_z.
const LayerTools& tools_for_layer(coordf_t print_z) const;
@@ -299,6 +365,8 @@ private:
void mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height);
void collect_extruder_statistics(bool prime_multi_material);
void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer);
void resolve_mixed_filaments(const PrintConfig &config);
void enforce_mixed_component_order();
// BBS
std::vector<unsigned int> generate_first_layer_tool_order(const Print& print);
@@ -311,8 +379,26 @@ private:
unsigned int m_last_printing_extruder = (unsigned int)-1;
// All extruders, which extrude some material over m_layer_tools.
std::vector<unsigned int> m_all_printing_extruders;
std::vector<unsigned int> m_used_mixed_filaments;
const DynamicPrintConfig* m_print_full_config = nullptr;
const PrintConfig* m_print_config_ptr = nullptr;
// Per-object gradient tracking: slot(0-based) -> PrintObject* -> list of layer indices
// where that object uses the slot. Populated by collect_extruders, consumed by resolve_mixed_filaments.
std::map<unsigned int, std::map<const PrintObject*, std::vector<size_t>>> m_mixed_object_layers;
// All layer indices (in m_layer_tools) where each object has any layer.
// Used by gradient run detection to distinguish real gaps (object has a layer
// that doesn't use the slot) from spurious gaps (another object's layer).
std::map<const PrintObject*, std::vector<size_t>> m_object_all_layer_indices;
// Per-volume gradient tracking: slot(0-based) -> (PrintObject*, ModelVolume id) -> list of
// layer indices where the given volume contributes to the slot. Populated by collect_extruders
// alongside m_mixed_object_layers when per_part gradient is enabled for the slot AND the
// ModelObject has >=2 model-part volumes using the slot. Empty for all other configurations,
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
// no-ops; downstream emission falls through to the per-object branch).
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
bool m_sorted = false;
+6
View File
@@ -210,6 +210,12 @@ void Layer::make_perimeters()
if (! (*it)->slices.empty()) {
LayerRegion* other_layerm = *it;
const PrintRegion &other_region = other_layerm->region();
// Per-part gradient tags a region with its owning ModelVolume; merging two
// differently-tagged regions would collapse volumes that need independent
// gradient runs. Both tags are invalid unless per-part gradient is on, so
// this is a no-op for every other configuration.
if (this_region.gradient_volume_id() != other_region.gradient_volume_id())
continue;
if (is_perimeter_compatible(*m_object->print(), this_region, other_region))
{
other_layerm->perimeters.clear();
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -352,7 +352,7 @@ void segment(CGALMesh& src, std::vector<CGALMesh>& dst, double smoothing_alpha =
//}
//else
{
dst.emplace_back(std::move(CGALMesh(out)));
dst.emplace_back(CGALMesh(out));
}
}
//if (mesh_merged.is_empty() == false) {
@@ -371,7 +371,7 @@ std::vector<TriangleMesh> segment(const TriangleMesh& src, double smoothing_alph
std::vector<TriangleMesh> out_meshes;
for (auto& outf_cgal_mesh: out_cgal_meshes)
{
out_meshes.emplace_back(std::move(cgal_to_triangle_mesh(outf_cgal_mesh.m)));
out_meshes.emplace_back(cgal_to_triangle_mesh(outf_cgal_mesh.m));
}
return out_meshes;
+129 -26
View File
@@ -1,6 +1,8 @@
#include "Model.hpp"
#include "libslic3r.h"
#include "BuildVolume.hpp"
#include "TexturePainting.hpp"
#include "Format/AssimpImport.hpp"
#include "ClipperUtils.hpp"
#include "Exception.hpp"
#include "Model.hpp"
@@ -104,6 +106,7 @@ Model& Model::assign_copy(const Model &rhs)
this->mk_version = rhs.mk_version;
this->md_name = rhs.md_name;
this->md_value = rhs.md_value;
this->texture_mesh = rhs.texture_mesh;
return *this;
}
@@ -139,6 +142,7 @@ Model& Model::assign_copy(Model &&rhs)
this->mk_version = rhs.mk_version;
this->md_name = rhs.md_name;
this->md_value = rhs.md_value;
this->texture_mesh = std::move(rhs.texture_mesh);
this->backup_path = std::move(rhs.backup_path);
this->object_backup_id_map = std::move(rhs.object_backup_id_map);
this->next_object_backup_id = rhs.next_object_backup_id;
@@ -239,6 +243,27 @@ _finished:
// BBS: add part plate related logic
// BBS: backup & restore
// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well.
// Build a plain geometry ModelObject from a textured mesh. The texture itself is carried
// separately on Model::texture_mesh and consumed by the texture import dialog.
static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mesh, const std::string& input_file)
{
std::string object_name = boost::filesystem::path(input_file).filename().string();
indexed_triangle_set its;
its.vertices.resize(tex_mesh.vertices.size());
for (size_t i = 0; i < tex_mesh.vertices.size(); ++i)
its.vertices[i] = Vec3f(tex_mesh.vertices[i][0], tex_mesh.vertices[i][1], tex_mesh.vertices[i][2]);
its.indices.resize(tex_mesh.indices.size());
for (size_t i = 0; i < tex_mesh.indices.size(); ++i)
its.indices[i] = Vec3i32(tex_mesh.indices[i][0], tex_mesh.indices[i][1], tex_mesh.indices[i][2]);
its_merge_vertices(its);
its_remove_degenerate_faces(its);
its_compactify_vertices(its);
model.add_object(object_name.c_str(), input_file.c_str(), TriangleMesh(std::move(its)));
}
Model Model::read_from_file(const std::string& input_file,
DynamicPrintConfig* config,
ConfigSubstitutionContext* config_substitutions,
@@ -281,32 +306,85 @@ Model Model::read_from_file(const std::string&
result = load_stl(input_file.c_str(), &model, nullptr, stlFn,256);
else if (boost::algorithm::iends_with(input_file, ".obj")) {
ObjInfo obj_info;
result = load_obj(input_file.c_str(), &model, obj_info, message);
if (result){
ObjDialogInOut in_out;
in_out.model = &model;
in_out.lost_material_name = obj_info.lost_material_name;
ObjParser::MtlData mtl_data;
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
// instead of the flat per-face colour dialog.
auto tex_mesh = std::make_shared<TexturedMesh>();
std::string obj_dir = boost::filesystem::path(input_file).parent_path().string();
if (obj_to_textured_mesh(obj_info,
model.objects.back()->volumes[0]->mesh().its,
mtl_data, obj_dir, *tex_mesh)) {
model.texture_mesh = tex_mesh;
}
}
else if (result && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
// Vertex-colour and MTL face-colour OBJs also go through the texture-to-color
// importer (as precomputed per-face colors) instead of the flat
// per-face colour dialog, matching the uv_png branch above.
auto build_tex_mesh_geometry = [&]() {
auto tex_mesh = std::make_shared<TexturedMesh>();
const auto& its = model.objects.back()->volumes[0]->mesh().its;
tex_mesh->vertices.resize(its.vertices.size());
for (size_t i = 0; i < its.vertices.size(); ++i)
tex_mesh->vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
tex_mesh->indices.resize(its.indices.size());
for (size_t i = 0; i < its.indices.size(); ++i)
tex_mesh->indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
return tex_mesh;
};
if (obj_info.vertex_colors.size() > 0) {
if (objFn) { // 1.result is ok and pop up a dialog
in_out.input_colors = std::move(obj_info.vertex_colors);
in_out.is_single_color = false;
in_out.deal_vertex_color = true;
objFn(in_out);
auto tex_mesh = build_tex_mesh_geometry();
const auto& its = model.objects.back()->volumes[0]->mesh().its;
tex_mesh->precomputed_face_colors.resize(its.indices.size());
for (size_t i = 0; i < its.indices.size(); ++i) {
const auto& f = its.indices[i];
auto avg = [&](int ch) -> std::size_t {
float v = (obj_info.vertex_colors[f[0]][ch]
+ obj_info.vertex_colors[f[1]][ch]
+ obj_info.vertex_colors[f[2]][ch]) / 3.0f * 255.0f;
return (std::size_t) std::clamp(v, 0.0f, 255.0f);
};
tex_mesh->precomputed_face_colors[i] = {avg(0), avg(1), avg(2)};
}
} else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { // mtl file
if (objFn) { // 1.result is ok and pop up a dialog
in_out.input_colors = std::move(obj_info.face_colors);
in_out.is_single_color = obj_info.is_single_mtl;
in_out.deal_vertex_color = false;
objFn(in_out);
tex_mesh->precomputed_vertex_colors = obj_info.vertex_colors;
model.texture_mesh = tex_mesh;
} else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) {
auto tex_mesh = build_tex_mesh_geometry();
const size_t nf = tex_mesh->indices.size();
tex_mesh->precomputed_face_colors.resize(nf);
for (size_t i = 0; i < nf; ++i) {
if (i < obj_info.face_colors.size()) {
const auto& c = obj_info.face_colors[i];
tex_mesh->precomputed_face_colors[i] = {
(std::size_t) std::clamp(c[0] * 255.0f, 0.0f, 255.0f),
(std::size_t) std::clamp(c[1] * 255.0f, 0.0f, 255.0f),
(std::size_t) std::clamp(c[2] * 255.0f, 0.0f, 255.0f)
};
} else {
tex_mesh->precomputed_face_colors[i] = {128, 128, 128};
}
}
} /*else if (obj_info.has_uv_png && obj_info.uvs.size() > 0) {
boost::filesystem::path full_path(input_file);
std::string obj_directory = full_path.parent_path().string();
obj_info.obj_dircetory = obj_directory;
result = false;
message = _L("Importing obj with png function is developing.");
}*/
model.texture_mesh = tex_mesh;
}
}
}
else if (boost::algorithm::iends_with(input_file, ".glb") ||
boost::algorithm::iends_with(input_file, ".gltf") ||
boost::algorithm::iends_with(input_file, ".fbx")) {
// These formats can carry material/texture data, so they go through the textured
// import path: the geometry becomes a normal object and the texture is handed to the
// texture-to-color dialog via Model::texture_mesh.
auto tex_mesh = std::make_shared<TexturedMesh>();
result = load_assimp_textured_model(input_file, *tex_mesh, &message);
if (result) {
model.texture_mesh = tex_mesh;
add_textured_mesh_to_model(model, *tex_mesh, input_file);
} else if (!message.empty()) {
BOOST_LOG_TRIVIAL(error) << "Assimp: failed to load model: " << message
<< ", path=" << input_file;
message = _L("The file format is incompatible and cannot be parsed.");
}
}
else if (boost::algorithm::iends_with(input_file, ".svg"))
@@ -578,6 +656,7 @@ void Model::clear_objects()
this->objects.clear();
object_backup_id_map.clear();
next_object_backup_id = 1;
texture_mesh.reset();
}
// BBS: backup, reuse objects
@@ -2576,7 +2655,8 @@ void ModelVolume::update_extruder_count(size_t extruder_count)
}
}
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id)
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id,
const std::vector<unsigned char> &filament_is_mixed)
{
std::vector<int> used_extruders = get_extruders();
for (int extruder_id : used_extruders) {
@@ -2587,8 +2667,22 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou
}
// Same stale-assignment cleanup as update_extruder_count, for the filament-delete path.
// Ported from BambuStudio (STUDIO-15763).
if (extruder_id() > extruder_count) {
this->config.erase("extruder");
size_t eid = extruder_id();
// Judge out-of-range against the post-remap id, mirroring update_filament_values_for_items_when_delete_filament.
// Using the pre-remap eid would wrongly erase a high extruder that should remap (e.g. 5 -> 4 after
// deleting filament 1); update_filament_values_for_items_when_delete_filament would then skip it
// (!has("extruder")) and the volume would fall back to the object default color.
size_t remapped = eid;
if (eid == filament_id)
remapped = (replace_filament_id > 0) ? (size_t)replace_filament_id : 1;
else if (eid > filament_id)
remapped = eid - 1;
if (remapped > extruder_count) {
// filament_is_mixed is the pre-delete snapshot; index it with the ORIGINAL eid (1-based),
// not remapped, so we check whether this volume's current slot is a mixed slot.
bool is_mixed = !filament_is_mixed.empty() && eid >= 1 && (eid - 1) < filament_is_mixed.size() && filament_is_mixed[eid - 1];
if (!is_mixed)
this->config.erase("extruder");
}
}
@@ -3495,6 +3589,15 @@ void FacetsAnnotation::get_facets(const ModelVolume& mv, std::vector<indexed_tri
selector.get_facets(facets_per_type);
}
void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta)
{
if (empty()) return;
TriangleSelector selector(mv.mesh());
selector.deserialize(m_data, false);
selector.shift_states_above(threshold, delta);
this->set(selector);
}
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
EnforcerBlockerType max_type,
EnforcerBlockerType to_delete_filament,
+11 -1
View File
@@ -47,6 +47,8 @@ namespace cereal {
}
namespace Slic3r {
struct TexturedMesh;
enum class ConversionType;
class BuildVolume;
@@ -740,6 +742,9 @@ public:
EnforcerBlockerType max_type,
EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE,
EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE);
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
bool empty() const { return m_data.triangles_to_split.empty(); }
@@ -932,7 +937,8 @@ public:
// BBS
std::vector<int> get_extruders() const;
void update_extruder_count(size_t extruder_count);
void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1);
void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1,
const std::vector<unsigned char> &filament_is_mixed = {});
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
@@ -1549,6 +1555,10 @@ public:
std::shared_ptr<ModelInfo> model_info = nullptr;
std::shared_ptr<ModelProfileInfo> profile_info = nullptr;
// Textured mesh data for texture-to-painting import. Populated by the loader when a mesh
// arrives with usable UVs and a texture map; consumed (and reset) by the import dialog.
std::shared_ptr<TexturedMesh> texture_mesh;
//makerlab information
std::string mk_name;
std::string mk_version;
+2
View File
@@ -1,4 +1,6 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "OpenVDBUtils.hpp"
#ifdef _MSC_VER
+17 -1
View File
@@ -229,6 +229,22 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
// Append thin walls to the nearest-neighbor search (only for first iteration)
if (! thin_walls.empty()) {
// Orca: apply fuzzy skin to thin walls as well
for (auto& thin_wall : thin_walls) {
// First, we convert the ThickPolyline into Arachne::ExtrusionLine so we could reuse our existing fuzzy code
Arachne::ExtrusionLine el(0, true);
el.junctions.reserve(thin_wall.points.size());
for (int i = 0; i < thin_wall.points.size(); i++) {
el.junctions.emplace_back(thin_wall.points[i], thin_wall.width[i], 0);
}
// Then we fuzzy it
apply_fuzzy_skin(&el, perimeter_generator, true, thin_wall.is_closed());
// Then convert the result back to ThickPolyline
thin_wall = Arachne::to_thick_polyline(el);
}
variable_width(thin_walls, erExternalPerimeter, perimeter_generator.ext_perimeter_flow, coll.entities);
thin_walls.clear();
}
@@ -392,7 +408,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
ExtrusionRole role = is_external ? erExternalPerimeter : erPerimeter;
const bool is_contour = !extrusion->is_closed || pg_extrusion.is_contour;
apply_fuzzy_skin(extrusion, perimeter_generator, is_contour);
apply_fuzzy_skin(extrusion, perimeter_generator, is_contour, extrusion->is_closed);
ExtrusionPaths paths;
// detect overhanging/bridging perimeters
+3
View File
@@ -8,7 +8,9 @@
#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif /* _MSC_VER */
@@ -1237,6 +1239,7 @@ static std::vector<std::string> s_Preset_print_options{
"flush_into_infill",
"flush_into_objects",
"flush_into_support",
"enable_mixed_color_sublayer",
"tree_support_branch_angle",
"tree_support_angle_slow",
"tree_support_wall_count",
+288 -49
View File
@@ -7,6 +7,7 @@
#include "PresetCacheFormat.hpp"
#include "PrintConfig.hpp"
#include "FilamentMixer.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
#include "Utils.hpp"
@@ -71,7 +72,17 @@ static std::vector<std::string> s_project_options {
// whether dynamic per-nozzle filament mapping is active. Persisted with the project and
// restored from a saved 3mf; reset to false on load and set true only by live device sync.
"has_filament_switcher",
"enable_filament_dynamic_map"
"enable_filament_dynamic_map",
// Mixed-color filament slots. Project-level parallel arrays indexed like filament_colour:
// which slots are virtual mixes, their component filaments, blend ratios and the optional
// Z-gradient description. Kept with the project so a saved 3mf round-trips the mix setup.
"filament_is_mixed",
"filament_mixed_components",
"filament_mixed_sublayer_ratios",
"filament_mixed_gradient",
"filament_mixed_gradient_range",
"filament_mixed_gradient_curve",
"filament_mixed_gradient_per_part"
};
//Orca: add custom as default
@@ -2710,6 +2721,79 @@ void PresetBundle::load_installed_sla_materials(AppConfig &config)
preset.set_visible_from_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)
{
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<std::string> parts;
auto load_bools = [&](const char *key) {
auto &vals = project_config.option<ConfigOptionBools>(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<ConfigOptionStrings>(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<ConfigOptionStrings>("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<std::string> 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();
@@ -2790,6 +2874,9 @@ void PresetBundle::update_selections(AppConfig &config)
auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast<double, std::string>);
project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end());
}
// 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,
@@ -2940,6 +3027,7 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast<double, std::string>);
project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end());
}
load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), 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,
@@ -3074,6 +3162,32 @@ void PresetBundle::export_selections(AppConfig &config)
"|");
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<unsigned char> &vals) {
std::string s;
for (size_t i = 0; i < vals.size(); ++i) {
if (i > 0) s += ",";
s += (vals[i] ? "1" : "0");
}
return s;
};
if (auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed"))
config.set_printer_setting(printer_name, "filament_is_mixed", join_bools(opt->values));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"))
config.set_printer_setting(printer_name, "filament_mixed_components", boost::algorithm::join(opt->values, "|"));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
config.set_printer_setting(printer_name, "filament_mixed_sublayer_ratios", boost::algorithm::join(opt->values, "|"));
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
config.set_printer_setting(printer_name, "filament_mixed_gradient", join_bools(opt->values));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
config.set_printer_setting(printer_name, "filament_mixed_gradient_range", boost::algorithm::join(opt->values, "|"));
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
config.set_printer_setting(printer_name, "filament_mixed_gradient_curve", escape_strings_cstyle(opt->values));
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
config.set_printer_setting(printer_name, "filament_mixed_gradient_per_part", join_bools(opt->values));
// BBS
//config.set("presets", "sla_print", sla_prints.get_selected_preset_name());
//config.set("presets", "sla_material", sla_materials.get_selected_preset_name());
@@ -3082,46 +3196,6 @@ void PresetBundle::export_selections(AppConfig &config)
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<std::string> 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<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
filament_color->resize(n);
// Sync filament multi colour
filament_multi_color->values.resize(n);
for (size_t i = 0; i < n; i++) {
filament_multi_color->values[i] = filament_color->values[i];
}
filament_color_type->resize(n);
filament_map->values.resize(n, 1);
filament_nozzle_map->values.resize(n, 0);
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
// 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_multi_color->values[i] = new_colors[i - old_filament_count];
filament_color_type->values[i] = "1"; // default color type
}
}
}
update_multi_material_filament_presets();
}
void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
{
unsigned old_filament_count = this->filament_presets.size();
@@ -3137,6 +3211,11 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
// Which slots are new is a fact about the arrays below, not about filament_presets:
// update_multi_material_filament_presets() tops that list up to the nozzle count on its own,
// so it can already sit at the new size while every array below is still at the old one.
const size_t old_slot_count = filament_color->values.size();
filament_color->resize(n);
// Sync filament multi colour
filament_multi_color->values.resize(n);
@@ -3149,14 +3228,29 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
// Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink
// with the filament count exactly like filament_colour above.
if (auto* opt = project_config.option<ConfigOptionBools>("filament_is_mixed"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
opt->values.resize(n, false);
//BBS set new filament color to new_color
if (old_filament_count < n) {
if (!new_color.empty()) {
for (unsigned i = old_filament_count; i < n; i++) {
filament_color->values[i] = new_color;
filament_multi_color->values[i] = new_color;
filament_color_type->values[i] = "1"; // default color type
}
if (!new_color.empty()) {
for (size_t i = old_slot_count; i < n; i++) {
filament_color->values[i] = new_color;
filament_multi_color->values[i] = new_color;
filament_color_type->values[i] = "1"; // default color type
}
}
@@ -3221,9 +3315,69 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id)
erase_or_resize(filament_color_type->values);
erase_or_resize(ams_multi_color_filment);
// Mixed-color metadata. Component IDs reference other slots by 1-based index, so a deleted
// *physical* filament must be remapped out of every mix before the arrays themselves shrink.
// Deleting a mixed slot needs no remap (nothing references a mixed slot as a component).
{
auto *is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto *comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_opt) {
bool del_is_physical = (to_del_flament_id >= is_mixed_opt->values.size()
|| !is_mixed_opt->values[to_del_flament_id]);
if (del_is_physical)
remap_mixed_components_on_delete(is_mixed_opt->values, comp_opt->values,
to_del_flament_id + 1);
}
if (is_mixed_opt)
erase_or_resize(is_mixed_opt->values);
if (comp_opt)
erase_or_resize(comp_opt->values);
}
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
erase_or_resize(opt->values);
if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"))
erase_or_resize(opt->values);
update_multi_material_filament_presets(to_del_flament_id);
}
bool PresetBundle::is_mixed_filament(size_t idx) const
{
auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
return opt && idx < opt->values.size() && opt->values[idx];
}
size_t PresetBundle::num_mixed_filaments() const
{
auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true));
}
// Counted off the mixed flags, not filament_presets: that list is topped up to the nozzle count on
// its own, so it can sit a slot ahead of the arrays that describe slots. Unlike the sibling
// physical_filament_config_indices(), which bounds by filament_presets, this ignores that top-up.
size_t PresetBundle::num_physical_filaments() const
{
const auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
return opt == nullptr ? filament_presets.size()
: size_t(std::count(opt->values.begin(), opt->values.end(), false));
}
std::vector<size_t> PresetBundle::physical_filament_config_indices() const
{
std::vector<size_t> indices;
for (size_t i = 0; i < filament_presets.size(); ++i)
if (!is_mixed_filament(i))
indices.push_back(i);
return indices;
}
// Orca: the AMS lookups below resolve a tray's filament_id to the FIRST compatible base
// preset. When several presets match the same id for the selected printer the pick is
@@ -3468,6 +3622,63 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("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<MixedSlotSnapshot> mixed_snapshots;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* mixed_comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
auto* mixed_ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
auto* mixed_gradient_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
auto* mixed_grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range");
auto* mixed_grad_curve_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
auto* mixed_per_part_opt = project_config.option<ConfigOptionBools>("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<AMSMapInfo> &infos, const AMSMapInfo &temp) {
for (int i = 0; i < infos.size(); i++) {
@@ -3631,7 +3842,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
return -1;
};
for (size_t i = 0; i < need_append_colors.size(); i++){
if (exist_filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)){
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);
@@ -3723,6 +3934,34 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
if (support_interface_filament_opt->value > 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();
+11 -2
View File
@@ -326,8 +326,9 @@ public:
// Export selections (current print, current filaments, current printer) into config.ini
void export_selections(AppConfig &config);
// BBS
void set_num_filaments(unsigned int n, std::vector<std::string> new_colors);
// n is the total slot count, and growth appends at the raw tail - which is where the mixed
// slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and
// then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does.
void set_num_filaments(unsigned int n, std::string new_col = "");
void update_num_filaments(unsigned int to_del_flament_id);
@@ -497,6 +498,14 @@ public:
// Read out the number of extruders from an active printer preset,
// update size and content of filament_presets.
void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1));
// Mixed-color filament slots: virtual slots realized from 2-3 physical filaments.
bool is_mixed_filament(size_t idx) const;
std::vector<size_t> physical_filament_config_indices() const;
// How many slots are mixed. They sit at the tail of the filament list and have no nozzle of
// their own, so any resize driven by the printer's extruder count has to add this on top.
size_t num_mixed_filaments() const;
// How many slots hold a real filament, i.e. everything ahead of the mixed tail.
size_t num_physical_filaments() const;
void on_extruders_count_changed(int extruder_count);
+116 -32
View File
@@ -5,6 +5,7 @@
#include "Brim.hpp"
#include "ClipperUtils.hpp"
#include "Extruder.hpp"
#include "FilamentMixer.hpp"
#include "Flow.hpp"
#include "Geometry/ConvexHull.hpp"
#include "I18N.hpp"
@@ -565,7 +566,7 @@ std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const
// If a wipe tower filament is explicitly set, ensure it participates in tool ordering.
if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) {
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament < int(config().nozzle_diameter.size()));
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size()));
extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based
}
@@ -1327,6 +1328,19 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (extruders.empty())
return { L("No extrusions under current settings.") };
// Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on;
// without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and
// the gradient is dropped silently. extruders() already covers painting, height ranges,
// per-feature filament ids and supports, and still lists mixed slots under their own id here.
if (!m_config.enable_mixed_color_sublayer.value) {
const auto &is_mixed = m_config.filament_is_mixed.values;
const auto &gradient = m_config.filament_mixed_gradient.values;
if (std::any_of(extruders.begin(), extruders.end(), [&](unsigned int e) {
return e < is_mixed.size() && is_mixed[e] && e < gradient.size() && gradient[e]; }))
warn(L("A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed."),
"enable_mixed_color_sublayer");
}
if (nozzles < 2 && extruders.size() > 1) {
auto ret = check_multi_filament_valid(*this);
if (!ret.string.empty())
@@ -1388,6 +1402,13 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
// #4043
if (total_copies_count > 1 && m_config.print_sequence != PrintSequence::ByObject)
return {L("Please select \"By object\" print sequence to print multiple objects in spiral vase mode."), nullptr, "spiral_mode"};
// A mixed (virtual) filament always resolves to multiple physical components, which
// spiral vase cannot print.
const auto &is_mixed = m_config.filament_is_mixed.values;
for (const PrintObject *object : m_objects)
for (unsigned int ext : object->object_extruders())
if (ext < is_mixed.size() && is_mixed[ext])
return {L("Spiral (vase) mode does not work when an object contains more than one material."), nullptr, "spiral_mode"};
assert(m_objects.size() == 1);
const auto all_regions = m_objects.front()->all_regions();
if (all_regions.size() > 1) {
@@ -1464,6 +1485,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
if (this->has_wipe_tower() && ! m_objects.empty()) {
// Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after
// resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here
// would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides
// mixed slots from the option; this guards loaded projects and the CLI.
if (m_config.wipe_tower_filament > 0) {
const auto &is_mixed = m_config.filament_is_mixed.values;
const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1);
if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx])
return { L("The wipe tower filament cannot be a mixed filament."), nullptr, "wipe_tower_filament" };
}
// Make sure all extruders use same diameter filament and have the same nozzle diameter
// EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments
double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front());
@@ -2585,18 +2617,31 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::vector<const PrintInstance*>::const_iterator print_object_instance_sequential_active;
std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> layers_to_print = GCode::collect_layers_to_print(*this);
std::vector<unsigned int> printExtruders;
// Per-object first-layer mixed-slot resolutions for the by-object remap below
// (BBS reads them from m_sequential_print_data->object_tool_ordering_map).
std::map<ObjectID, std::map<unsigned int, unsigned int>> seq_mixed_resolution;
// Cleared on every process so a print-sequence or selector-mode change can never leave
// stale object pointers behind; repopulated below only by the sequential selector path.
m_sequential_dynamic_orderings.clear();
if (this->config().print_sequence == PrintSequence::ByObject) {
// Order object instances for sequential print.
print_object_instances_ordering = sort_object_instances_by_model_order(*this);
// A mixed slot is virtual; only its components reach a nozzle. These per-object orderings
// are unsorted (no resolve_mixed_filaments), so expand the slots here for the grouping, the
// unprintable sets and the slice-used lists. Because the expansion happens here rather than
// on the sorted orderings, the first-layer used set lists every component of a mixed slot,
// not just the one layer 0 resolves to. No-op without mixed filaments.
const auto &is_mixed = m_config.filament_is_mixed.values;
const auto &comp_strs = m_config.filament_mixed_components.values;
const bool has_mixed = has_any_mixed_filament(is_mixed);
std::vector<unsigned int> first_layer_used_filaments;
std::vector<std::vector<unsigned int>> all_filaments;
for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) {
auto& layer_filament = tool_ordering.layer_tools()[idx].extruders;
auto layer_filament = tool_ordering.layer_tools()[idx].extruders;
if (has_mixed)
layer_filament = expand_mixed_filaments(layer_filament, is_mixed, comp_strs);
all_filaments.emplace_back(layer_filament);
if (idx == 0)
first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end());
@@ -2608,6 +2653,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments);
auto geometric_unprintables = this->get_geometric_unprintable_filaments();
if (has_mixed)
expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs);
auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments);
// Selector (per-layer regroup) prints skip the static grouping: their print-wide result
// is stitched from the per-object plans after the ordering loop below.
@@ -2659,6 +2706,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::vector<std::vector<int>> nozzle_map_per_layer;
std::vector<std::vector<unsigned int>> stitched_layer_filaments;
print_object_instance_sequential_active = print_object_instances_ordering.begin();
std::vector<unsigned int> used_mixed_filaments;
for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object;
if (dynamic_reorder) {
@@ -2687,11 +2735,18 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
} else {
tool_ordering = ToolOrdering(*print_object, initial_extruder_id);
tool_ordering.sort_and_build_data(*print_object, initial_extruder_id);
if (!tool_ordering.layer_tools().empty())
seq_mixed_resolution[print_object->id()] = tool_ordering.layer_tools().front().mixed_filament_resolution;
}
// Only sorted orderings have run resolve_mixed_filaments, so only they know which
// mixed slots actually print.
append(used_mixed_filaments, tool_ordering.used_mixed_filaments());
if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast<unsigned int>(-1)) {
append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders);
}
}
sort_remove_duplicates(used_mixed_filaments);
this->set_slice_used_mixed_filaments(used_mixed_filaments);
if (dynamic_reorder && m_objects.size() > 1) {
// Stitch the per-object plans into one print-wide selector result. A single-object
// sequential print publishes (and writes back) from its own ordering instead: the
@@ -2712,6 +2767,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
first_layer_used_filaments = tool_ordering.layer_tools().front().extruders;
this->set_slice_used_filaments(first_layer_used_filaments, tool_ordering.all_extruders());
this->set_slice_used_mixed_filaments(tool_ordering.used_mixed_filaments());
has_wipe_tower = this->has_wipe_tower() && tool_ordering.has_wipe_tower();
initial_extruder_id = tool_ordering.first_extruder();
print_object_instances_ordering = chain_print_object_instances(*this);
@@ -2719,6 +2775,28 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
auto objectExtruderMap = getObjectExtruderMap(*this);
// Resolve mixed filament virtual slots to physical components so brim
// extruder matching works correctly (mixed slot IDs are not present
// in printExtruders after ToolOrdering::resolve_mixed_filaments).
{
const LayerTools *first_lt = nullptr;
if (m_config.print_sequence != PrintSequence::ByObject && !tool_ordering.layer_tools().empty())
first_lt = &tool_ordering.layer_tools().front();
for (auto &[obj_id, ext_1based] : objectExtruderMap) {
if (ext_1based == 0)
continue;
const std::map<unsigned int, unsigned int> *resolution = nullptr;
if (first_lt)
resolution = &first_lt->mixed_filament_resolution;
else if (auto obj_it = seq_mixed_resolution.find(obj_id); obj_it != seq_mixed_resolution.end())
resolution = &obj_it->second;
if (resolution) {
auto it = resolution->find(ext_1based - 1);
if (it != resolution->end())
ext_1based = it->second + 1;
}
}
}
std::vector<std::pair<ObjectID, unsigned int>> objPrintVec;
for (const PrintInstance* instance : print_object_instances_ordering) {
const ObjectID& print_object_ID = instance->print_object->id();
@@ -3712,7 +3790,7 @@ std::vector<Polygons> Print::get_extruder_printable_polygons() const
Polygons ploys = {Polygon::new_scale(e_printable_area)};
extruder_printable_polys.emplace_back(ploys);
}
return std::move(extruder_printable_polys);
return extruder_printable_polys;
}
std::vector<Polygons> Print::get_extruder_unprintable_polygons() const
@@ -3725,7 +3803,7 @@ std::vector<Polygons> Print::get_extruder_unprintable_polygons() const
Polygons ploys = diff(printable_poly, Polygon::new_scale(e_printable_area));
extruder_unprintable_polys.emplace_back(ploys);
}
return std::move(extruder_unprintable_polys);
return extruder_unprintable_polys;
}
size_t Print::get_extruder_id(unsigned int filament_id) const
@@ -3776,6 +3854,14 @@ bool Print::is_dynamic_group_reorder() const
const bool enabled = opt && opt->value;
if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1)
return false;
// Dynamic regrouping and mixed-color slots are incompatible: a mixed slot is resolved to
// different physical components per layer, so a group assignment made up-front would be wrong.
const auto &is_mixed = m_config.filament_is_mixed.values;
for (unsigned int filament_id : extruders()) {
if (filament_id < is_mixed.size() && is_mixed[filament_id])
return false;
}
return true;
}
@@ -3999,38 +4085,36 @@ void Print::_make_wipe_tower()
return;
// Check whether there are any layers in m_tool_ordering, which are marked with has_wipe_tower,
// they print neither object, nor support. These layers are above the raft and below the object, and they
// shall be added to the support layers to be printed.
// see https://github.com/prusa3d/PrusaSlicer/issues/607
// they print neither object, nor support. Each such layer needs a virtual support layer
// counterpart in m_objects.front() so that GCode::collect_layers_to_print picks it up and the
// wipe tower G-code is actually emitted for that z. Such layers appear in two scenarios:
// - above the raft, between raft top and the first real object layer
// (see https://github.com/prusa3d/PrusaSlicer/issues/607);
// - between two real wipe-tower layers, when one object is fully floating above another and
// the support_top_z_distance / support_bottom_z_distance gap leaves interior z values with
// neither object nor support (continuity fill in ToolOrdering::fill_wipe_tower_partitions).
// The previous implementation only handled the first contiguous run starting at the first
// virtual layer, which made the second scenario silently produce empty wipe-tower layers.
{
size_t idx_begin = size_t(-1);
size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
// Find the first wipe tower layer, which does not have a counterpart in an object or a support layer.
auto &support_layers = m_objects.front()->support_layers();
auto it_layer = support_layers.begin();
const size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
for (size_t i = 0; i < idx_end; ++ i) {
const LayerTools &lt = 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 &lt = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
break;
lt.has_support = true;
// Insert the new support layer.
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
LayerTools &lt = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
continue;
while (it_layer != support_layers.end() && (*it_layer)->print_z + EPSILON < lt.print_z)
++ it_layer;
if (it_layer != support_layers.end() && std::abs((*it_layer)->print_z - lt.print_z) < EPSILON) {
lt.has_support = true;
++ it_layer;
continue;
}
lt.has_support = true;
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
++ it_layer;
}
}
this->throw_if_canceled();
+23 -4
View File
@@ -117,9 +117,9 @@ class PrintRegion
public:
PrintRegion() = default;
PrintRegion(const PrintRegionConfig &config);
PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {}
PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {}
PrintRegion(PrintRegionConfig &&config);
PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {}
PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {}
~PrintRegion() = default;
// Methods NOT modifying the PrintRegion's state:
@@ -129,6 +129,10 @@ public:
// Identifier of this PrintRegion in the list of Print::m_print_regions.
int print_region_id() const throw() { return m_print_region_id; }
int print_object_region_id() const throw() { return m_print_object_region_id; }
// Volume identity used to differentiate same-config regions when per-part gradient is enabled.
// Default-constructed (invalid) means this region is not tied to a specific volume — preserves
// existing behavior for all paths not using per_part_gradient.
ObjectID gradient_volume_id() const throw() { return m_gradient_volume_id; }
// 1-based extruder identifier for this region and role.
unsigned int extruder(FlowRole role) const;
Flow flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer = false) const;
@@ -158,6 +162,10 @@ private:
int m_print_region_id { -1 };
int m_print_object_region_id { -1 };
int m_ref_cnt { 0 };
// Per-part gradient: when non-invalid, this region belongs exclusively to one ModelVolume,
// letting same-color volumes within a combined ModelObject be tracked separately for gradient
// emission. Default invalid -> region keying behaves exactly as before.
ObjectID m_gradient_volume_id;
};
inline bool operator==(const PrintRegion &lhs, const PrintRegion &rhs) { return lhs.config_hash() == rhs.config_hash() && lhs.config() == rhs.config(); }
@@ -306,6 +314,11 @@ public:
Transform3d trafo_bboxes;
std::vector<ObjectID> cached_volume_ids;
// Per-part gradient: the slot_per_part_enabled bit vector that produced these regions.
// Print::apply compares it against the current one to detect a change that PrintRegionConfig
// alone would not reveal, and regenerates the regions when it differs.
std::vector<bool> last_slot_per_part_enabled;
void ref_cnt_inc() { ++ m_ref_cnt; }
void ref_cnt_dec() { if (-- m_ref_cnt == 0) delete this; }
void clear() {
@@ -930,8 +943,8 @@ public:
// If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r).
std::string export_gcode(const std::string& path_template, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
//return 0 means successful
int export_cached_data(const std::string& dir_path, bool with_space=false);
int load_cached_data(const std::string& directory);
int export_cached_data(const std::string& dir_path, bool with_space=false) override;
int load_cached_data(const std::string& directory) override;
// methods for handling state
bool is_step_done(PrintStep step) const { return Inherited::is_step_done(step); }
@@ -1075,6 +1088,10 @@ public:
m_slice_used_filaments = used_filaments;
}
std::vector<unsigned int> get_slice_used_filaments(bool first_layer) const { return first_layer ? m_slice_used_filaments_first_layer : m_slice_used_filaments;}
void set_slice_used_mixed_filaments(const std::vector<unsigned int> &used_mixed_filaments) {
m_slice_used_mixed_filaments = used_mixed_filaments;
}
const std::vector<unsigned int>& get_slice_used_mixed_filaments() const { return m_slice_used_mixed_filaments; }
/**
* @brief Determines the unprintable filaments for each extruder based on its physical attributes
@@ -1342,6 +1359,8 @@ private:
std::vector<unsigned int> m_slice_used_filaments;
std::vector<unsigned int> m_slice_used_filaments_first_layer;
// 0-based mixed (virtual) filament slots actually used on this plate.
std::vector<unsigned int> m_slice_used_mixed_filaments;
//BBS: plate's origin
Vec3d m_origin {0, 0, 0};
+124 -10
View File
@@ -1,6 +1,7 @@
#include "ClipperUtils.hpp"
#include "Model.hpp"
#include "Print.hpp"
#include "FilamentMixer.hpp"
#include <boost/log/trivial.hpp>
#include <cfloat>
@@ -886,7 +887,12 @@ bool verify_update_print_object_regions(
size_t hash = regions[i]->config_hash();
size_t j = i;
for (++ j; j < regions.size() && regions[j]->config_hash() == hash; ++ j)
if (regions[i]->config() == regions[j]->config()) {
// Same config but different gradient_volume_id is intentional (per-part gradient
// splitting) and must NOT be flagged as a merge. When per-part is off all regions
// carry an invalid (default) gradient_volume_id, so the AND condition is always
// true and behavior matches the legacy check.
if (regions[i]->config() == regions[j]->config()
&& regions[i]->gradient_volume_id() == regions[j]->gradient_volume_id()) {
// Regions were merged. We need to reslice.
return false;
}
@@ -978,7 +984,10 @@ static PrintObjectRegions* generate_print_object_regions(
const float xy_contour_compensation,
const std::vector<unsigned int> &painting_extruders,
std::vector<int> &variant_index,
const bool has_painted_fuzzy_skin)
const bool has_painted_fuzzy_skin,
// Per-part gradient: slot_per_part_enabled[s-1] is true when mixed slot s has
// filament_mixed_gradient_per_part on. Empty / all-false preserves legacy behavior.
const std::vector<bool> &slot_per_part_enabled = {})
{
// Reuse the old object or generate a new one.
auto out = print_object_regions_old ? std::unique_ptr<PrintObjectRegions>(print_object_regions_old) : std::make_unique<PrintObjectRegions>();
@@ -1013,19 +1022,71 @@ static PrintObjectRegions* generate_print_object_regions(
update_volume_bboxes(layer_ranges_regions, out->cached_volume_ids, model_volumes, out->trafo_bboxes, is_mm_painted ? 0.f : std::max(0.f, xy_contour_compensation));
std::vector<PrintRegion*> region_set;
auto get_create_region = [&region_set, &all_regions](PrintRegionConfig &&config) -> PrintRegion* {
// Look up or create a PrintRegion. The optional volume_tag, when valid (non-zero ObjectID),
// keys the region to one ModelVolume so two volumes with identical settings still get
// separate regions — needed so each part can run its own gradient. A default (invalid)
// tag reproduces the previous lookup exactly.
auto get_create_region = [&region_set, &all_regions](PrintRegionConfig &&config, ObjectID volume_tag = ObjectID()) -> PrintRegion* {
size_t hash = config.hash();
auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash](const PrintRegion* l) {
return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config); });
if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config)
auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash, volume_tag](const PrintRegion* l) {
return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config)
|| (l->config_hash() == hash && l->config() == config && l->gradient_volume_id() < volume_tag); });
if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config
&& (*it)->gradient_volume_id() == volume_tag)
return *it;
// Insert into a sorted array, it has O(n) complexity, but the calling algorithm has an O(n^2*log(n)) complexity anyways.
all_regions.emplace_back(std::make_unique<PrintRegion>(std::move(config), hash, int(all_regions.size())));
all_regions.emplace_back(std::make_unique<PrintRegion>(std::move(config), hash, int(all_regions.size()), volume_tag));
PrintRegion *region = all_regions.back().get();
region_set.emplace(it, region);
return region;
};
// Per-part gradient: count how many model-part volumes in this object use each
// per-part-enabled gradient slot. Only slots with at least 2 users get their volumes
// tagged — a single-user slot gains nothing from per-volume splitting and would only
// inflate the region count. Empty slot_per_part_enabled leaves this empty, so
// compute_volume_tag below always returns an invalid tag and nothing changes.
std::vector<int> per_part_volume_users;
if (!slot_per_part_enabled.empty()) {
per_part_volume_users.assign(slot_per_part_enabled.size(), 0);
for (const ModelVolume *mv : model_volumes) {
if (! mv->is_model_part())
continue;
const DynamicPrintConfig *range_cfg = layer_ranges_regions.empty() ? nullptr : layer_ranges_regions.front().config;
PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, range_cfg, *mv, num_extruders, variant_index);
for (unsigned int s_1based : { (unsigned int)vol_cfg.outer_wall_filament_id.value,
(unsigned int)vol_cfg.inner_wall_filament_id.value,
(unsigned int)vol_cfg.sparse_infill_filament_id.value,
(unsigned int)vol_cfg.internal_solid_filament_id.value,
(unsigned int)vol_cfg.top_surface_filament_id.value,
(unsigned int)vol_cfg.bottom_surface_filament_id.value }) {
if (s_1based >= 1
&& size_t(s_1based - 1) < slot_per_part_enabled.size()
&& slot_per_part_enabled[s_1based - 1])
++per_part_volume_users[s_1based - 1];
}
}
}
auto compute_volume_tag = [&](const PrintRegionConfig &cfg, const ModelVolume &mv) -> ObjectID {
if (per_part_volume_users.empty())
return ObjectID();
auto qualifies = [&](unsigned int s_1based) {
return s_1based >= 1
&& size_t(s_1based - 1) < slot_per_part_enabled.size()
&& slot_per_part_enabled[s_1based - 1]
&& per_part_volume_users[s_1based - 1] >= 2;
};
if (qualifies((unsigned int)cfg.outer_wall_filament_id.value)
|| qualifies((unsigned int)cfg.inner_wall_filament_id.value)
|| qualifies((unsigned int)cfg.sparse_infill_filament_id.value)
|| qualifies((unsigned int)cfg.internal_solid_filament_id.value)
|| qualifies((unsigned int)cfg.top_surface_filament_id.value)
|| qualifies((unsigned int)cfg.bottom_surface_filament_id.value)) {
return mv.id();
}
return ObjectID();
};
// Chain the regions in the order they are stored in the volumes list.
for (int volume_id = 0; volume_id < int(model_volumes.size()); ++ volume_id) {
const ModelVolume &volume = *model_volumes[volume_id];
@@ -1034,9 +1095,11 @@ static PrintObjectRegions* generate_print_object_regions(
if (const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, volume); bbox) {
if (volume.is_model_part()) {
// Add a model volume, assign an existing region or generate a new one.
PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index);
ObjectID volume_tag = compute_volume_tag(vol_cfg, volume);
layer_range.volume_regions.push_back({
&volume, -1,
get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index)),
get_create_region(std::move(vol_cfg), volume_tag),
bbox
});
} else if (volume.is_negative_volume()) {
@@ -1121,6 +1184,12 @@ static PrintObjectRegions* generate_print_object_regions(
}
}
// Save the slot_per_part_enabled bit vector that produced these regions, so the guard in
// Print::apply can detect changes on the next call even when PrintRegionConfig did not
// change. Always written — including an empty vector — so the snapshot always reflects
// the exact input used to generate the current regions.
out->last_slot_per_part_enabled = slot_per_part_enabled;
return out.release();
}
@@ -1141,6 +1210,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
std::vector <unsigned int> used_filaments = this->extruders(true);
std::unordered_set <unsigned int> used_filament_set(used_filaments.begin(), used_filaments.end());
// A mixed slot is virtual: the filaments actually consumed are its components, so add them
// to the used set or they would be treated as unused and stripped from the config.
{
auto* is_mixed_opt = new_full_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = new_full_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
auto expanded = expand_mixed_filaments(used_filaments, is_mixed_opt->values, comp_strs_opt->values);
used_filament_set.insert(expanded.begin(), expanded.end());
}
}
//new_full_config.normalize_fdm(used_filaments);
new_full_config.normalize_fdm_1();
t_config_option_keys changed_keys = new_full_config.normalize_fdm_2(objects().size(), used_filaments.size());
@@ -1802,6 +1882,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
update_filament_self_index_cache();
}
// Per-part gradient: compute the per-slot enable bit vector once for this Print::apply pass.
// Used by generate_print_object_regions to decide which volumes deserve their own PrintRegion.
std::vector<bool> slot_per_part_enabled;
{
const auto &is_mixed_vec = m_config.filament_is_mixed.values;
const auto &grad_vec = m_config.filament_mixed_gradient.values;
const auto &per_part_vec = m_config.filament_mixed_gradient_per_part.values;
const auto &components_vec = m_config.filament_mixed_components.values;
slot_per_part_enabled.assign(is_mixed_vec.size(), false);
for (size_t i = 0; i < is_mixed_vec.size(); ++i) {
if (! is_mixed_vec[i])
continue;
std::vector<unsigned int> comps = parse_mixed_components(i < components_vec.size() ? components_vec[i] : "");
if (comps.size() != 2)
continue;
if (i >= grad_vec.size() || ! grad_vec[i])
continue;
if (i >= per_part_vec.size() || ! per_part_vec[i])
continue;
slot_per_part_enabled[i] = true;
}
}
// All regions now have distinct settings.
// Check whether applying the new region config defaults we would get different regions,
// update regions or create regions from scratch.
@@ -1828,7 +1931,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (const ModelVolume *volume : volumes) {
const std::vector<bool> &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states;
assert(volume_used_facet_states.size() == used_facet_states.size());
// Paint data saved before the painted state range was extended deserializes a
// shorter used_states vector, so merge over the common prefix.
for (size_t state_idx = 0; state_idx < std::min(volume_used_facet_states.size(), used_facet_states.size()); ++state_idx)
used_facet_states[state_idx] |= volume_used_facet_states[state_idx];
}
@@ -1862,6 +1966,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys));
},
print_variant_index)) {
// Per-part gradient: PrintRegionConfig alone cannot reveal a change in which slots
// have per-part enabled, so compare against the snapshot taken when these regions
// were generated and regenerate on any difference (slot toggled, per-part moved
// between slots, eligibility changed via components / gradient / is_mixed).
if (print_object_regions->last_slot_per_part_enabled != slot_per_part_enabled) {
invalidate();
model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid;
print_regions_reshuffled = true;
}
// Regions are valid, just keep them.
} else {
// Regions were reshuffled.
@@ -1884,7 +1997,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value),
painting_extruders,
print_variant_index,
print_object.is_fuzzy_skin_painted());
print_object.is_fuzzy_skin_painted(),
slot_per_part_enabled);
}
for (auto it = it_print_object; it != it_print_object_end; ++it)
if ((*it)->m_shared_regions) {
+91 -1
View File
@@ -2,6 +2,7 @@
#include "PrintConfigConstants.hpp"
#include "ClipperUtils.hpp"
#include "Config.hpp"
#include "FilamentMixer.hpp"
#include "MaterialType.hpp"
#include "I18N.hpp"
#include "format.hpp"
@@ -3263,6 +3264,62 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBools { false });
// Mixed-color filament. A slot flagged here is virtual: it is not loaded into any
// physical extruder, but resolved at slicing time into the physical filaments listed
// in filament_mixed_components, blended either by splitting each layer into
// sub-layers or by alternating whole layers (see enable_mixed_color_sublayer).
def = this->add("filament_is_mixed", coBools);
def->label = L("Is mixed filament");
def->tooltip = L("Whether this filament slot is a mixed filament composed of multiple physical filaments");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBools{false});
def = this->add("filament_mixed_components", coStrings);
def->label = L("Mixed filament components");
def->tooltip = L("Comma-separated 1-based indices of component filaments, e.g. \"1,3\"");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_sublayer_ratios", coStrings);
def->label = L("Mixed filament sublayer ratios");
def->tooltip = L("Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_gradient", coBools);
def->label = L("Mixed filament gradient");
def->tooltip = L("Enable Z-direction gradient mode for mixed filament sub-layers. "
"When enabled, the sub-layer ratios vary linearly across layers.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBools{false});
def = this->add("filament_mixed_gradient_range", coStrings);
def->label = L("Mixed filament gradient range");
def->tooltip = L("Start and end ratios for the first component in gradient mode. "
"Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_gradient_curve", coStrings);
def->label = L("Mixed filament gradient curve");
def->tooltip = L("Optional Photoshop-style custom curve mapping Z progress to the first "
"component ratio. Encoded as pipe-separated control points, "
"either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override "
"is needed (empty token or \"nan\" means use PCHIP default). "
"x in [0,1]; y is clamped to the configured ratio range, "
"e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear "
"gradient_range is used instead.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings{""});
def = this->add("filament_mixed_gradient_per_part", coBools);
def->label = L("Mixed filament per-part gradient");
def->tooltip = L("When gradient mode is enabled, apply the gradient to each part of an "
"assembly independently rather than treating the whole assembly as one "
"Z range.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBools{false});
// defined in bits
// 0 means cannot support, 1 means support
// 0 bit: can support in left extruder
@@ -7402,6 +7459,14 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloats { 1. });
def = this->add("enable_mixed_color_sublayer", coBool);
def->label = L("Mixed color sublayer");
def->tooltip = L("Enable mixed color sublayer splitting. When enabled, layers containing mixed color "
"filaments will be split into sub-layers to achieve color mixing effects.");
def->category = L("Quality");
def->mode = comSimple;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("enable_prime_tower", coBool);
def->label = L("Enable");
def->tooltip = L("The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects.");
@@ -9605,7 +9670,15 @@ t_config_option_keys DynamicPrintConfig::normalize_fdm_2(int num_objects, int us
ConfigOptionBool *enable_wrapping_opt = this->option<ConfigOptionBool>("enable_wrapping_detection");
bool enable_wrapping = enable_wrapping_opt != nullptr && enable_wrapping_opt->value;
if (!is_smooth_timelapse && !enable_wrapping && (used_filaments == 1 || (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) {
bool has_mixed_filament = false;
{
auto *mixed_opt = this->option<ConfigOptionBools>("filament_is_mixed");
if (mixed_opt)
has_mixed_filament = has_any_mixed_filament(mixed_opt->values);
}
if (!is_smooth_timelapse && !enable_wrapping
&& ( (used_filaments == 1 && !has_mixed_filament)
|| (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) {
if (ept_opt->value) {
ept_opt->value = false;
changed_keys.push_back("enable_prime_tower");
@@ -11753,6 +11826,23 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
}
}
// Mixed-color (混色) parameter validation.
{
const auto &is_mixed = cfg.filament_is_mixed.values;
const auto &comp_strs = cfg.filament_mixed_components.values;
const auto &ratio_strs = cfg.filament_mixed_sublayer_ratios.values;
const auto &gradient_flags = cfg.filament_mixed_gradient.values;
const auto &range_strs = cfg.filament_mixed_gradient_range.values;
const auto &curve_strs = cfg.filament_mixed_gradient_curve.values;
std::map<std::string, std::string> mixed_errors = validate_mixed_filament_params(
is_mixed, comp_strs, ratio_strs, gradient_flags,
range_strs, curve_strs);
for (const auto &kv : mixed_errors)
if (error_message.find(kv.first) == error_message.end())
error_message.emplace(kv.first, kv.second);
}
// The configuration is valid.
return error_message;
}
+9
View File
@@ -1538,6 +1538,14 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, filament_colour))
((ConfigOptionStrings, filament_vendor))
((ConfigOptionBools, filament_is_support))
// Mixed-color filament: a virtual slot realized from 2-3 physical filaments.
((ConfigOptionBools, filament_is_mixed))
((ConfigOptionStrings, filament_mixed_components))
((ConfigOptionStrings, filament_mixed_sublayer_ratios))
((ConfigOptionBools, filament_mixed_gradient))
((ConfigOptionStrings, filament_mixed_gradient_range))
((ConfigOptionStrings, filament_mixed_gradient_curve))
((ConfigOptionBools, filament_mixed_gradient_per_part))
((ConfigOptionInts, filament_printable))
((ConfigOptionInts, filament_extruder_compatibility))
((ConfigOptionFloats, filament_change_length))
@@ -1838,6 +1846,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionInts, nozzle_temperature_range_low))
((ConfigOptionInts, nozzle_temperature_range_high))
((ConfigOptionFloats, wipe_distance))
((ConfigOptionBool, enable_mixed_color_sublayer))
((ConfigOptionBool, enable_prime_tower))
((ConfigOptionBool, prime_tower_enable_framework))
// BBS: change wipe_tower_x and wipe_tower_y data type to floats to add partplate logic
+1 -1
View File
@@ -906,7 +906,7 @@ void PrintObject::detect_overhangs_for_lift()
Layer& lower_layer = *layer.lower_layer;
ExPolygons overhangs = diff_ex(layer.lslices, offset_ex(lower_layer.lslices, scale_(min_overlap)));
layer.loverhangs = std::move(offset2_ex(overhangs, -0.1f * scale_(line_width), 0.1f * scale_(line_width)));
layer.loverhangs = offset2_ex(overhangs, -0.1f * scale_(line_width), 0.1f * scale_(line_width));
layer.loverhangs_bbox = get_extents(layer.loverhangs);
}
});
+2
View File
@@ -1,4 +1,6 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
#include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
+1 -1
View File
@@ -199,7 +199,7 @@ static void MakeMesh(TopoDS_Shape& theSolid, TriangleMesh& theMesh)
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
aPnt.Transform(aTrsf);
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
}
//BBS: copy triangles
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
+14 -14
View File
@@ -842,7 +842,7 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
// normal overhang
ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS);
overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted));
overhangs_all_layers[layer_nr] = diff_ex(curr_polys, lower_layer_offseted);
double duration{ std::chrono::duration_cast<second_>(clock_::now() - t0).count() };
if (duration > 30 || overhangs_all_layers[layer_nr].size() > 100) {
@@ -1396,7 +1396,7 @@ void TreeSupport::generate_toolpaths()
raft_areas.push_back(expoly);
}
raft_areas = std::move(offset_ex(raft_areas, scale_(object_config.raft_first_layer_expansion)));
raft_areas = offset_ex(raft_areas, scale_(object_config.raft_first_layer_expansion));
size_t layer_nr = 0;
for (; layer_nr < m_slicing_params.base_raft_layers; layer_nr++) {
@@ -1522,9 +1522,9 @@ void TreeSupport::generate_toolpaths()
erSupportMaterialInterface : erSupportMaterial;
make_perimeter_and_inner_brim(ts_layer->support_fills.entities, poly, wall_count, flow,
brim_role);
polys = std::move(offset_ex(poly, -flow.scaled_spacing()));
polys = offset_ex(poly, -flow.scaled_spacing());
} else if (area_group.type == SupportLayer::Roof1stLayer) {
polys = std::move(offset_ex(poly, 0.5*support_flow.scaled_width()));
polys = offset_ex(poly, 0.5*support_flow.scaled_width());
}
else {
polys.push_back(poly);
@@ -2269,7 +2269,7 @@ void TreeSupport::draw_circles()
// Inside the gap: remove only the part overlapping the contact surface, keep the rest.
if (bottom_gap_height > EPSILON && layer_bottom_z < band_gap_top - EPSILON) {
any_gap_cleared = true;
comp_poly = std::move(diff_ex(comp_poly, band.surfaces));
comp_poly = diff_ex(comp_poly, band.surfaces);
}
// Overlaps interface band
@@ -2304,7 +2304,7 @@ void TreeSupport::draw_circles()
ExPolygons comp_interface = band_ex.empty() ? ExPolygons {} : intersection_ex(comp_poly, band_ex);
if (!comp_interface.empty()) {
append(new_floor_areas, comp_interface);
comp_poly = std::move(diff_ex(comp_poly, offset_ex(comp_interface, 10)));
comp_poly = diff_ex(comp_poly, offset_ex(comp_interface, 10));
}
}
@@ -2396,7 +2396,7 @@ void TreeSupport::draw_circles()
ts_layer->lslices.emplace_back(*expoly);
}
ts_layer->lslices = std::move(union_ex(ts_layer->lslices));
ts_layer->lslices = union_ex(ts_layer->lslices);
//Must update bounding box which is used in avoid crossing perimeter
ts_layer->lslices_bboxes.clear();
ts_layer->lslices_bboxes.reserve(ts_layer->lslices.size());
@@ -2474,7 +2474,7 @@ void TreeSupport::draw_circles()
if (global_lightning_infill)
{
//search overhangs globally
overhang = std::move(diff_ex(offset_ex(base_areas_lower, -2.0 * scale_(support_extrusion_width)), base_areas));
overhang = diff_ex(offset_ex(base_areas_lower, -2.0 * scale_(support_extrusion_width)), base_areas);
}
else
{
@@ -2485,13 +2485,13 @@ void TreeSupport::draw_circles()
Polygon rev_hole = hole;
rev_hole.make_counter_clockwise();
ExPolygons ex_hole;
ex_hole.emplace_back(std::move(ExPolygon(rev_hole)));
ex_hole.emplace_back(ExPolygon(rev_hole));
for (auto& other_area : base_areas)
//if (&other_area != &base_area)
ex_hole = std::move(diff_ex(ex_hole, other_area));
overhang = std::move(union_ex(overhang, ex_hole));
ex_hole = diff_ex(ex_hole, other_area);
overhang = union_ex(overhang, ex_hole);
}
overhang = std::move(intersection_ex(overhang, offset_ex(base_areas_lower, -0.5 * scale_(support_extrusion_width))));
overhang = intersection_ex(overhang, offset_ex(base_areas_lower, -0.5 * scale_(support_extrusion_width)));
}
overhangs.emplace_back(to_polygons(overhang));
@@ -2746,7 +2746,7 @@ void TreeSupport::drop_nodes()
m_object->print()->set_status(60 + int(10 * (1 - float(layer_nr) / contact_nodes.size())), _u8L("Generating support"));// (boost::format(_u8L("Support: propagate branches at layer %d")) % layer_nr).str());
Polygons layer_contours = std::move(m_ts_data->get_contours_with_holes(obj_layer_nr));
Polygons layer_contours = m_ts_data->get_contours_with_holes(obj_layer_nr);
//std::unordered_map<Line, bool, LineHash>& mst_line_x_layer_contour_cache = m_mst_line_x_layer_contour_caches[layer_nr];
tbb::concurrent_unordered_map<Line, bool, LineHash> mst_line_x_layer_contour_cache;
auto is_line_cut_by_contour = [&mst_line_x_layer_contour_cache,&layer_contours](Point a, Point b)
@@ -3763,7 +3763,7 @@ const ExPolygons& TreeSupportData::calculate_avoidance(const RadiusLayerPair& ke
}
const ExPolygons &collision = get_collision(radius, layer_nr);
avoidance_areas.insert(avoidance_areas.end(), collision.begin(), collision.end());
avoidance_areas = std::move(union_ex(avoidance_areas));
avoidance_areas = union_ex(avoidance_areas);
auto ret = m_avoidance_cache.insert({key, std::move(avoidance_areas)});
//assert(ret.second);
return ret.first->second;
+726
View File
@@ -0,0 +1,726 @@
#include "TexturePainting.hpp"
#include <algorithm>
#include <cmath>
#include <map>
#include <set>
#include <utility>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <boost/log/trivial.hpp>
#include "TextureToColor/TextureToColor.hpp"
#include "TextureToColor/ColorUtils.hpp"
#include "Model.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
static cv::Mat decode_texture_image(const TextureImage& img) {
if (img.data.empty())
return {};
// Raw encoded image data (PNG/JPEG) from glTF loader: width == -1
if (img.width <= 0 || img.height <= 0) {
std::vector<unsigned char> buf(img.data.begin(), img.data.end());
cv::Mat raw(1, static_cast<int>(buf.size()), CV_8UC1, buf.data());
cv::Mat decoded = cv::imdecode(raw, cv::IMREAD_COLOR);
return decoded;
}
int cv_type = (img.channels == 4) ? CV_8UC4 : CV_8UC3;
std::vector<unsigned char> pixel_buf(img.data.begin(), img.data.end());
cv::Mat src(img.height, img.width, cv_type, pixel_buf.data());
cv::Mat bgr;
if (img.channels == 4)
cv::cvtColor(src, bgr, cv::COLOR_RGBA2BGR);
else if (img.channels == 3)
cv::cvtColor(src, bgr, cv::COLOR_RGB2BGR);
else
return {};
return bgr;
}
static void build_tex2color_mesh(
const TexturedMesh& textured,
tex2color::TriMesh& mesh,
std::vector<std::vector<Vec2f>>& uv_coords)
{
const size_t nv = textured.vertices.size();
const size_t nf = textured.indices.size();
mesh.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i) {
mesh.vertices[i] = Vec3f(
textured.vertices[i][0],
textured.vertices[i][1],
textured.vertices[i][2]);
}
mesh.indices.resize(nf);
for (size_t i = 0; i < nf; ++i) {
mesh.indices[i] = Vec3i32(
textured.indices[i][0],
textured.indices[i][1],
textured.indices[i][2]);
}
uv_coords.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
uv_coords[fi].resize(3);
for (int vi = 0; vi < 3; ++vi) {
if (textured.has_face_uvs()) {
int uv_idx = textured.uv_indices[fi][vi];
if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) {
uv_coords[fi][vi] = Vec2f(
textured.uv_coords[uv_idx][0],
textured.uv_coords[uv_idx][1]);
} else {
uv_coords[fi][vi] = Vec2f(0.f, 0.f);
}
} else {
int vtx_idx = textured.indices[fi][vi];
if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) {
uv_coords[fi][vi] = Vec2f(
textured.uvs[vtx_idx][0],
textured.uvs[vtx_idx][1]);
} else {
uv_coords[fi][vi] = Vec2f(0.f, 0.f);
}
}
}
}
}
static void extract_painted_mesh(
const tex2color::TriMesh& color_mesh,
const std::vector<std::array<std::size_t,3>>& face_colors,
PaintedMesh& painted)
{
const size_t nv = color_mesh.vertices.size();
const size_t nf = color_mesh.indices.size();
painted.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i) {
const auto& v = color_mesh.vertices[i];
painted.vertices[i] = {v.x(), v.y(), v.z()};
}
painted.indices.resize(nf);
for (size_t i = 0; i < nf; ++i) {
const auto& f = color_mesh.indices[i];
painted.indices[i] = {f[0], f[1], f[2]};
}
painted.face_colors = face_colors;
std::set<std::array<std::size_t,3>> unique_colors(face_colors.begin(), face_colors.end());
painted.cluster_colors.assign(unique_colors.begin(), unique_colors.end());
}
// Build a vertically-stacked atlas from multiple textures and remap per-face UVs.
//
// Sub-textures are laid out left-aligned (x=0) at successive y offsets, with
// atlas_w taken as the maximum width across all sub-textures. UVs must therefore
// be remapped on BOTH axes so that faces belonging to a sub-texture narrower
// than atlas_w sample inside that sub-texture's region (left side of the atlas)
// instead of the right-side zero-padding. Materials that carry only a baseColor
// (no map_Kd / glTF baseColorTexture) get their own 1x1 swatch at the bottom of
// the atlas so their faces sample the correct flat colour rather than being
// silently aliased onto textures[0].
static bool build_multi_texture_atlas(
const TexturedMesh& textured,
cv::Mat& out_atlas,
std::vector<std::vector<Vec2f>>& out_uv_coords)
{
std::vector<cv::Mat> decoded;
decoded.reserve(textured.textures.size());
for (const auto& ti : textured.textures)
decoded.push_back(decode_texture_image(ti));
const bool has_mapping = !textured.material_texture_map.empty();
const size_t nf = textured.indices.size();
auto resolve_tex_idx = [&](int mat_idx) -> int {
if (!has_mapping || mat_idx < 0
|| static_cast<size_t>(mat_idx) >= textured.material_texture_map.size())
return -1;
const int ti = textured.material_texture_map[mat_idx];
if (ti < 0 || static_cast<size_t>(ti) >= decoded.size() || decoded[ti].empty())
return -1;
return ti;
};
// Determine atlas width (max width across all textures) and per-texture row offsets.
int atlas_w = 0;
int atlas_h = 0;
std::vector<int> y_offsets(decoded.size(), 0);
int first_usable_tex = -1;
for (size_t i = 0; i < decoded.size(); ++i) {
if (decoded[i].empty()) continue;
if (first_usable_tex < 0) first_usable_tex = static_cast<int>(i);
y_offsets[i] = atlas_h;
atlas_w = std::max(atlas_w, decoded[i].cols);
atlas_h += decoded[i].rows;
}
if (atlas_w == 0 || atlas_h == 0)
return false;
// Collect materials that have a baseColor but no usable texture so we can
// route their faces to a dedicated 1x1 solid swatch instead of aliasing
// them onto textures[0].
std::map<int, int> mat_solid_y; // mat_idx -> y row in atlas
std::map<int, std::array<float,4>> mat_solid_color; // mat_idx -> baseColor (RGBA)
for (size_t fi = 0; fi < nf; ++fi) {
const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
if (mat_idx < 0) continue;
if (resolve_tex_idx(mat_idx) >= 0) continue;
if (static_cast<size_t>(mat_idx) >= textured.material_colors.size()) continue;
if (mat_solid_y.find(mat_idx) != mat_solid_y.end()) continue;
mat_solid_y[mat_idx] = atlas_h++;
mat_solid_color[mat_idx] = textured.material_colors[mat_idx];
}
out_atlas = cv::Mat::zeros(atlas_h, atlas_w, CV_8UC3);
for (size_t i = 0; i < decoded.size(); ++i) {
if (decoded[i].empty()) continue;
cv::Mat roi = out_atlas(cv::Rect(0, y_offsets[i], decoded[i].cols, decoded[i].rows));
decoded[i].copyTo(roi);
}
for (const auto& kv : mat_solid_color) {
const auto& c = kv.second;
// OpenCV stores BGR; baseColor is RGBA in [0,1].
out_atlas.at<cv::Vec3b>(mat_solid_y[kv.first], 0) = cv::Vec3b(
static_cast<uchar>(std::clamp(c[2] * 255.f, 0.f, 255.f)),
static_cast<uchar>(std::clamp(c[1] * 255.f, 0.f, 255.f)),
static_cast<uchar>(std::clamp(c[0] * 255.f, 0.f, 255.f)));
}
out_uv_coords.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
const int tex_idx = resolve_tex_idx(mat_idx);
// Pick the atlas region this face samples from.
int y_off = 0, x_off = 0, th = atlas_h, tw = atlas_w;
bool use_solid = false;
if (tex_idx >= 0) {
y_off = y_offsets[tex_idx];
th = decoded[tex_idx].rows;
tw = decoded[tex_idx].cols;
} else if (mat_idx >= 0 && mat_solid_y.count(mat_idx) > 0) {
y_off = mat_solid_y[mat_idx];
th = 1;
tw = 1;
use_solid = true;
} else if (first_usable_tex >= 0) {
// Last-resort fallback: faces without a material or without any
// baseColor still need somewhere to sample; the first usable
// texture preserves legacy behaviour and, with the per-axis
// remapping below, no longer aliases onto the zero-padded right
// margin even when sub-textures have unequal widths.
y_off = y_offsets[first_usable_tex];
th = decoded[first_usable_tex].rows;
tw = decoded[first_usable_tex].cols;
}
out_uv_coords[fi].resize(3);
for (int vi = 0; vi < 3; ++vi) {
float u = 0.f, v = 0.f;
if (textured.has_face_uvs()) {
int uv_idx = textured.uv_indices[fi][vi];
if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) {
u = textured.uv_coords[uv_idx][0];
v = textured.uv_coords[uv_idx][1];
}
} else {
int vtx_idx = textured.indices[fi][vi];
if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) {
u = textured.uvs[vtx_idx][0];
v = textured.uvs[vtx_idx][1];
}
}
if (use_solid) {
// Aim at the centre of the 1x1 swatch so bilinear sampling
// (in tex2color) cannot drift into neighbouring rows.
const float u_atlas = (x_off + 0.5f) / static_cast<float>(atlas_w);
const float v_atlas = (y_off + 0.5f) / static_cast<float>(atlas_h);
out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas);
} else {
// Wrap to [0,1) on both axes (OBJ tile UVs may step outside
// the unit square), then scale by the sub-texture extents so
// samples land inside its actual region. Without scaling u,
// any sub-texture narrower than atlas_w would have all its
// faces sampled from the right-side zero-padding.
u = u - std::floor(u);
v = v - std::floor(v);
const float u_atlas = (x_off + u * tw) / static_cast<float>(atlas_w);
const float v_atlas = (y_off + v * th) / static_cast<float>(atlas_h);
out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas);
}
}
}
return true;
}
bool texture_to_painting(
const TexturedMesh& textured,
PaintedMesh& painted,
const TexturePaintingSettings& settings,
PaintProgressCallback progress,
PaintCancelCallback cancel)
{
if (textured.vertices.empty() || textured.indices.empty() || textured.textures.empty())
return false;
cv::Mat texture;
tex2color::TriMesh input_mesh;
std::vector<std::vector<Vec2f>> uv_coords;
const bool multi_tex = textured.textures.size() > 1 && !textured.material_texture_map.empty();
if (multi_tex) {
if (!build_multi_texture_atlas(textured, texture, uv_coords))
return false;
// Build mesh geometry (atlas UVs already computed above)
const size_t nv = textured.vertices.size();
const size_t nf = textured.indices.size();
input_mesh.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i)
input_mesh.vertices[i] = Vec3f(
textured.vertices[i][0], textured.vertices[i][1], textured.vertices[i][2]);
input_mesh.indices.resize(nf);
for (size_t i = 0; i < nf; ++i)
input_mesh.indices[i] = Vec3i32(
textured.indices[i][0], textured.indices[i][1], textured.indices[i][2]);
} else {
texture = decode_texture_image(textured.textures[0]);
if (texture.empty())
return false;
build_tex2color_mesh(textured, input_mesh, uv_coords);
}
tex2color::TextureToColorSettings algo_settings;
algo_settings.target_colors_num = settings.target_colors_num;
algo_settings.smooth_weight = settings.smooth_weight;
algo_settings.oversampling_iters = settings.oversampling_iters;
switch (settings.mesh_repair_decision) {
case TexturePaintingSettings::MeshRepairDecision::Ask:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask;
break;
case TexturePaintingSettings::MeshRepairDecision::RepairAndImport:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport;
break;
case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair:
default:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair;
break;
}
tex2color::AlgoProgressCallback algo_progress = nullptr;
if (progress) {
algo_progress = [&progress](tex2color::AlgoProgress p) {
progress(p.percent, p.message);
};
}
tex2color::AlgoCancelCallback algo_cancel = nullptr;
if (cancel) {
algo_cancel = [&cancel]() -> bool { return cancel(); };
}
tex2color::TriMesh color_mesh;
std::vector<std::array<std::size_t,3>> face_colors;
algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required;
algo_settings.mesh_repair_callback = settings.mesh_repair_callback;
bool ok = tex2color::TextureToColor(
input_mesh, uv_coords, texture,
color_mesh, face_colors,
algo_settings, algo_progress, algo_cancel);
if (!ok)
return false;
extract_painted_mesh(color_mesh, face_colors, painted);
return true;
}
bool face_colors_to_painting(
const TexturedMesh& mesh,
PaintedMesh& painted,
const TexturePaintingSettings& settings,
PaintProgressCallback progress,
PaintCancelCallback cancel)
{
if (mesh.vertices.empty() || mesh.indices.empty() || mesh.precomputed_face_colors.empty())
return false;
// Build tex2color::TriMesh from input geometry
tex2color::TriMesh input_mesh;
input_mesh.vertices.resize(mesh.vertices.size());
for (size_t i = 0; i < mesh.vertices.size(); ++i)
input_mesh.vertices[i] = Vec3f(mesh.vertices[i][0], mesh.vertices[i][1], mesh.vertices[i][2]);
input_mesh.indices.resize(mesh.indices.size());
for (size_t i = 0; i < mesh.indices.size(); ++i)
input_mesh.indices[i] = Vec3i32(mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2]);
// Forward settings to tex2color
tex2color::TextureToColorSettings algo_settings;
algo_settings.target_colors_num = settings.target_colors_num;
algo_settings.smooth_weight = settings.smooth_weight;
switch (settings.mesh_repair_decision) {
case TexturePaintingSettings::MeshRepairDecision::Ask:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask;
break;
case TexturePaintingSettings::MeshRepairDecision::RepairAndImport:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport;
break;
case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair:
default:
algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair;
break;
}
algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required;
algo_settings.mesh_repair_callback = settings.mesh_repair_callback;
tex2color::AlgoProgressCallback algo_progress = nullptr;
if (progress) {
algo_progress = [&progress](tex2color::AlgoProgress p) {
progress(p.percent, p.message);
};
}
tex2color::AlgoCancelCallback algo_cancel = nullptr;
if (cancel) {
algo_cancel = [&cancel]() -> bool { return cancel(); };
}
tex2color::TriMesh out_mesh;
std::vector<std::array<std::size_t,3>> out_face_colors;
bool ok = tex2color::ClusterAndSmooth(
input_mesh, mesh.precomputed_face_colors, out_mesh, out_face_colors,
algo_settings, algo_progress, algo_cancel,
mesh.precomputed_vertex_colors);
if (!ok)
return false;
extract_painted_mesh(out_mesh, out_face_colors, painted);
return true;
}
double compute_delta_e(
const std::array<std::size_t,3>& rgb1,
const std::array<float,4>& rgba2)
{
return tex2color::color_utils::calc_rgb_color_difference_by_ciede2000(
rgb1,
{
static_cast<std::size_t>(rgba2[0] * 255.0f),
static_cast<std::size_t>(rgba2[1] * 255.0f),
static_cast<std::size_t>(rgba2[2] * 255.0f)
});
}
std::vector<FilamentMatch> match_clusters_to_filaments(
const std::vector<std::array<std::size_t,3>>& cluster_colors,
const std::vector<std::array<float,4>>& filament_colors,
const std::vector<std::string>& /*filament_names*/)
{
std::vector<FilamentMatch> matches(cluster_colors.size());
for (size_t ci = 0; ci < cluster_colors.size(); ++ci) {
matches[ci].cluster_index = static_cast<int>(ci);
matches[ci].cluster_color = cluster_colors[ci];
matches[ci].delta_e = 1e9;
for (size_t fi = 0; fi < filament_colors.size(); ++fi) {
double de = compute_delta_e(cluster_colors[ci], filament_colors[fi]);
if (de < matches[ci].delta_e) {
matches[ci].delta_e = de;
matches[ci].filament_index = static_cast<int>(fi);
matches[ci].filament_color = filament_colors[fi];
}
}
}
return matches;
}
bool apply_painted_mesh_to_volume(
const PaintedMesh& painted,
const std::vector<FilamentMatch>& matches,
ModelVolume& volume)
{
if (painted.face_colors.empty() || matches.empty())
return false;
const auto& cluster_colors = painted.cluster_colors;
std::map<std::array<std::size_t,3>, int> color_to_filament;
for (const auto& m : matches) {
if (m.cluster_index >= 0 && m.cluster_index < (int)cluster_colors.size() && m.filament_index >= 0)
color_to_filament[cluster_colors[m.cluster_index]] = m.filament_index;
}
indexed_triangle_set its;
its.vertices.resize(painted.vertices.size());
for (size_t i = 0; i < painted.vertices.size(); ++i) {
its.vertices[i] = Vec3f(
painted.vertices[i][0],
painted.vertices[i][1],
painted.vertices[i][2]);
}
its.indices.resize(painted.indices.size());
for (size_t i = 0; i < painted.indices.size(); ++i) {
its.indices[i] = Vec3i32(
painted.indices[i][0],
painted.indices[i][1],
painted.indices[i][2]);
}
TriangleMesh new_mesh(std::move(its));
// The volume already went through ModelObject::add_volume ->
// center_geometry_after_creation, which translated its mesh by
// -source.mesh_offset (and folded that shift into the volume
// transformation). The painted mesh, however, is derived from the
// raw textured mesh and is therefore expressed in the original
// un-centered coordinate frame. Reuse the exact recorded shift to
// align it -- do NOT compute it from the bounding-box centers of
// the two meshes: tex2color::TextureToColor performs subdivision
// and CGAL polygon-soup repair, so the painted vertex count and
// bbox no longer match the original textured mesh and a bbox-
// center alignment would silently displace the geometry.
//
// If the model has been scaled by Model::convert_from_meters /
// convert_from_imperial_units after load, the painted mesh fed
// here is already in millimetres (Model::convert_* also scales
// texture_mesh in place) while source.mesh_offset was recorded
// before the conversion and therefore still lives in the original
// pre-scaled frame. Bring it into the same frame as the painted
// vertices so the alignment shift below stays correct on the
// textured-import path. This compensation is scoped to this
// function so that other (non-textured) import paths are not
// affected.
Vec3d mesh_offset = volume.source.mesh_offset;
double unit_scale = 1.0;
if (volume.source.is_converted_from_meters)
unit_scale = 1000.0;
else if (volume.source.is_converted_from_inches)
unit_scale = 25.4;
if (unit_scale != 1.0)
mesh_offset *= unit_scale;
if (!mesh_offset.isApprox(Vec3d::Zero()))
new_mesh.translate(-mesh_offset.cast<float>());
new_mesh.set_init_shift(mesh_offset);
// Log bbox drift for diagnostics. Subdivision + CGAL polygon-soup
// repair routinely changes vertex count and bbox, so moderate drift
// is expected and must not block the apply.
if (!new_mesh.empty() && !volume.mesh().empty()) {
const Vec3d new_center = new_mesh.bounding_box().center();
const Vec3d cur_center = volume.mesh().bounding_box().center();
const double diag = volume.mesh().bounding_box().size().norm();
const double drift = (new_center - cur_center).norm();
if (drift > 0.05 * std::max(1.0, diag))
BOOST_LOG_TRIVIAL(warning)
<< "apply_painted_mesh_to_volume: painted bbox center drifted by "
<< drift << " (bbox diag=" << diag
<< ", unit_scale=" << unit_scale
<< ", from_meters=" << volume.source.is_converted_from_meters
<< ", from_inches=" << volume.source.is_converted_from_inches << ")";
else if (drift > 1e-3 * std::max(1.0, diag))
BOOST_LOG_TRIVIAL(info)
<< "apply_painted_mesh_to_volume: minor bbox drift "
<< drift << " (bbox diag=" << diag
<< ", unit_scale=" << unit_scale << ")";
}
volume.set_mesh(std::move(new_mesh));
volume.calculate_convex_hull();
// Re-center the replaced mesh so its bbox center sits at the origin,
// matching what center_geometry_after_creation did for the original mesh.
// CGAL repair / subdivision may shift the bbox center (drift); without
// re-centering, the volume offset (which was computed for the original
// centered mesh) no longer matches, causing the model to float or clip.
// Pass false to keep source.mesh_offset unchanged.
volume.center_geometry_after_creation(false);
volume.invalidate_convex_hull_2d();
// Mesh geometry has been replaced; any per-face annotation indexed
// against the previous triangle set is now stale. mmu_segmentation_facets
// is rewritten below from the new selector; reset the others so future
// import paths that carry support / seam / fuzzy_skin painting cannot
// leak indices from the old mesh into the new one.
volume.supported_facets.reset();
volume.fuzzy_skin_facets.reset();
volume.seam_facets.reset();
if (ModelObject* obj = volume.get_object())
obj->invalidate_bounding_box();
TriangleSelector selector(volume.mesh());
for (size_t fi = 0; fi < painted.face_colors.size() && fi < (size_t)volume.mesh().its.indices.size(); ++fi) {
auto it = color_to_filament.find(painted.face_colors[fi]);
if (it != color_to_filament.end()) {
int extruder_idx = it->second;
auto state = static_cast<EnforcerBlockerType>(
static_cast<int>(EnforcerBlockerType::Extruder1) + extruder_idx);
if (state <= EnforcerBlockerType::ExtruderMax)
selector.set_facet(static_cast<int>(fi), state);
}
}
volume.mmu_segmentation_facets.set(selector);
return true;
}
bool decode_texture_to_pixels(
const TextureImage& img,
std::vector<unsigned char>& out_pixels,
int& out_w, int& out_h)
{
cv::Mat decoded = decode_texture_image(img);
if (decoded.empty())
return false;
// decoded is BGR, CV_8UC3
out_w = decoded.cols;
out_h = decoded.rows;
size_t nbytes = (size_t)out_w * out_h * 3;
out_pixels.resize(nbytes);
if (decoded.isContinuous()) {
std::memcpy(out_pixels.data(), decoded.data, nbytes);
} else {
for (int r = 0; r < out_h; ++r)
std::memcpy(out_pixels.data() + r * out_w * 3, decoded.ptr(r), out_w * 3);
}
return true;
}
// Sample face color from texture using 3 explicit UV values (centroid + bilinear).
static std::array<std::size_t,3> sample_face_from_uvs(
const cv::Mat& tex,
const std::array<float,2>& uv0,
const std::array<float,2>& uv1,
const std::array<float,2>& uv2)
{
float cu = (uv0[0] + uv1[0] + uv2[0]) / 3.f;
float cv_val = (uv0[1] + uv1[1] + uv2[1]) / 3.f;
cu = cu - std::floor(cu);
cv_val = cv_val - std::floor(cv_val);
float fx = cu * (tex.cols - 1);
float fy = cv_val * (tex.rows - 1);
int x0 = std::clamp(static_cast<int>(fx), 0, tex.cols - 1);
int y0 = std::clamp(static_cast<int>(fy), 0, tex.rows - 1);
int x1 = std::min(x0 + 1, tex.cols - 1);
int y1 = std::min(y0 + 1, tex.rows - 1);
float wx = fx - x0;
float wy = fy - y0;
const int ch = tex.channels();
auto sample = [&](int row, int col) -> std::array<float,3> {
const uchar* ptr = tex.data + row * tex.step[0] + col * ch;
return {static_cast<float>(ptr[2]), static_cast<float>(ptr[1]), static_cast<float>(ptr[0])};
};
auto c00 = sample(y0, x0);
auto c10 = sample(y0, x1);
auto c01 = sample(y1, x0);
auto c11 = sample(y1, x1);
std::array<std::size_t,3> color;
for (int i = 0; i < 3; ++i) {
float top = c00[i] * (1.f - wx) + c10[i] * wx;
float bot = c01[i] * (1.f - wx) + c11[i] * wx;
color[i] = static_cast<std::size_t>(std::clamp(top * (1.f - wy) + bot * wy, 0.f, 255.f));
}
return color;
}
// Legacy overload: look up UVs from per-vertex array by vertex indices.
static std::array<std::size_t,3> sample_face_from_texture(
const cv::Mat& tex,
const std::vector<std::array<float,2>>& uvs,
const std::array<int,3>& face)
{
std::array<float,2> uv0 = {0.f, 0.f}, uv1 = {0.f, 0.f}, uv2 = {0.f, 0.f};
if (face[0] >= 0 && static_cast<size_t>(face[0]) < uvs.size()) uv0 = uvs[face[0]];
if (face[1] >= 0 && static_cast<size_t>(face[1]) < uvs.size()) uv1 = uvs[face[1]];
if (face[2] >= 0 && static_cast<size_t>(face[2]) < uvs.size()) uv2 = uvs[face[2]];
return sample_face_from_uvs(tex, uv0, uv1, uv2);
}
bool sample_original_face_colors(
const TexturedMesh& textured,
std::vector<std::array<std::size_t,3>>& out_face_colors)
{
if (textured.indices.empty())
return false;
// Decode all textures up front
std::vector<cv::Mat> decoded_textures;
decoded_textures.reserve(textured.textures.size());
for (const auto& ti : textured.textures) {
decoded_textures.push_back(decode_texture_image(ti));
}
const bool has_mapping = !textured.material_texture_map.empty();
const size_t nf = textured.indices.size();
out_face_colors.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1;
int tex_idx = -1;
if (has_mapping && mat_idx >= 0 && static_cast<size_t>(mat_idx) < textured.material_texture_map.size())
tex_idx = textured.material_texture_map[mat_idx];
else if (!decoded_textures.empty())
tex_idx = 0; // fallback: single-texture model
if (tex_idx >= 0 && static_cast<size_t>(tex_idx) < decoded_textures.size()
&& !decoded_textures[tex_idx].empty()) {
if (textured.has_face_uvs()) {
const auto& ui = textured.uv_indices[fi];
auto get_uv = [&](int vi) -> std::array<float,2> {
int idx = ui[vi];
if (idx >= 0 && static_cast<size_t>(idx) < textured.uv_coords.size())
return textured.uv_coords[idx];
return {0.f, 0.f};
};
out_face_colors[fi] = sample_face_from_uvs(
decoded_textures[tex_idx], get_uv(0), get_uv(1), get_uv(2));
} else {
out_face_colors[fi] = sample_face_from_texture(
decoded_textures[tex_idx], textured.uvs, textured.indices[fi]);
}
} else if (has_mapping && mat_idx >= 0
&& static_cast<size_t>(mat_idx) < textured.material_colors.size()) {
// No texture — use baseColorFactor as solid color
const auto& c = textured.material_colors[mat_idx];
out_face_colors[fi] = {
static_cast<std::size_t>(std::clamp(c[0] * 255.f, 0.f, 255.f)),
static_cast<std::size_t>(std::clamp(c[1] * 255.f, 0.f, 255.f)),
static_cast<std::size_t>(std::clamp(c[2] * 255.f, 0.f, 255.f))
};
} else {
out_face_colors[fi] = {192, 192, 192}; // default gray
}
}
return true;
}
} // namespace Slic3r
+137
View File
@@ -0,0 +1,137 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include <vector>
struct indexed_triangle_set;
namespace Slic3r {
class TriangleMesh;
class ModelVolume;
struct TextureImage {
int width = 0;
int height = 0;
int channels = 4;
std::vector<unsigned char> data;
};
struct TexturedMesh {
std::vector<std::array<float,3>> vertices;
std::vector<std::array<int,3>> indices;
std::vector<std::array<float,2>> uvs;
std::vector<TextureImage> textures;
std::vector<int> material_ids;
// material index -> index in textures[] (-1 if no texture, use material_colors)
std::vector<int> material_texture_map;
// per-material baseColorFactor (RGBA 0-1), indexed by material index
std::vector<std::array<float,4>> material_colors;
// Per-face independent UV support (for OBJ where the same vertex can have
// different texture coordinates on different faces).
std::vector<std::array<float,2>> uv_coords; // UV coordinate pool
std::vector<std::array<int,3>> uv_indices; // per-face UV indices into uv_coords
bool has_face_uvs() const { return !uv_indices.empty() && !uv_coords.empty(); }
// Pre-computed per-face colors (e.g. from OBJ vertex colors or MTL Kd).
// When non-empty, the pipeline skips texture decode/sample/oversample and
// consumes these instead of sampling a texture.
// Each entry is {R, G, B} in [0..255].
std::vector<std::array<std::size_t,3>> precomputed_face_colors;
// Per-vertex colors from OBJ (RGBA, [0..1]), indexed by vertex index.
// On a low-poly mesh these are quantized into a small palette and the mesh is
// split along the resulting cluster boundaries, so color borders stay sharp
// instead of being averaged away into a single color per face.
std::vector<std::array<float,4>> precomputed_vertex_colors;
};
struct PaintedMesh {
std::vector<std::array<float,3>> vertices;
std::vector<std::array<int,3>> indices;
std::vector<std::array<std::size_t,3>> face_colors; // per-face RGB [0..255]
std::vector<std::array<std::size_t,3>> cluster_colors;
};
using PaintProgressCallback = std::function<void(int percent, const char* message)>;
using PaintCancelCallback = std::function<bool()>;
using PaintMeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh,
indexed_triangle_set& repaired_mesh,
std::function<void(const char* message, unsigned progress)> progress_callback,
std::function<bool()> cancel_callback,
std::string* error_message)>;
struct TexturePaintingSettings {
std::size_t target_colors_num = 4;
double smooth_weight = 0.5;
std::size_t oversampling_iters = 0;
enum class MeshRepairDecision {
Ask,
ImportWithoutRepair,
RepairAndImport
};
MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair;
bool* mesh_repair_decision_required = nullptr;
PaintMeshRepairCallback mesh_repair_callback;
};
struct FilamentMatch {
int cluster_index = -1;
int filament_index = -1;
double delta_e = 0.0;
std::array<std::size_t,3> cluster_color = {0,0,0};
std::array<float,4> filament_color = {0,0,0,1};
};
bool texture_to_painting(
const TexturedMesh& textured,
PaintedMesh& painted,
const TexturePaintingSettings& settings = {},
PaintProgressCallback progress = nullptr,
PaintCancelCallback cancel = nullptr);
// Turn pre-computed per-face colors into a painted mesh, skipping texture decode
// and UV sampling. A low-poly mesh that also carries precomputed_vertex_colors is
// split along quantized color boundaries, which replaces its geometry.
bool face_colors_to_painting(
const TexturedMesh& mesh,
PaintedMesh& painted,
const TexturePaintingSettings& settings = {},
PaintProgressCallback progress = nullptr,
PaintCancelCallback cancel = nullptr);
std::vector<FilamentMatch> match_clusters_to_filaments(
const std::vector<std::array<std::size_t,3>>& cluster_colors,
const std::vector<std::array<float,4>>& filament_colors,
const std::vector<std::string>& filament_names);
double compute_delta_e(
const std::array<std::size_t,3>& rgb1,
const std::array<float,4>& rgba2);
bool apply_painted_mesh_to_volume(
const PaintedMesh& painted,
const std::vector<FilamentMatch>& matches,
ModelVolume& volume);
// Decode a TextureImage (which may contain raw PNG/JPEG bytes) into BGR pixel data.
// On success, populates out_pixels (BGR, 3 bytes/pixel) and sets out_w/out_h.
bool decode_texture_to_pixels(
const TextureImage& img,
std::vector<unsigned char>& out_pixels,
int& out_w, int& out_h);
// Sample per-face colors from the correct texture per material_ids.
// Uses material_texture_map / material_colors for multi-material GLBs.
// Falls back to textures[0] when the mapping is absent.
bool sample_original_face_colors(
const TexturedMesh& textured,
std::vector<std::array<std::size_t,3>>& out_face_colors);
} // namespace Slic3r
@@ -0,0 +1,15 @@
#pragma once
#include <functional>
namespace Slic3r { namespace tex2color {
struct AlgoProgress {
int percent = 0;
const char* message = "";
};
using AlgoProgressCallback = std::function<void(AlgoProgress)>;
using AlgoCancelCallback = std::function<bool()>;
} // namespace tex2color
} // namespace Slic3r
+173
View File
@@ -0,0 +1,173 @@
#pragma once
#include "TriMesh.hpp"
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Polygon_mesh_processing/repair.h>
#include <chrono>
#include <cstdio>
#include <map>
#include <queue>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace Slic3r { namespace tex2color {
namespace cgalutils {
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using CGALMesh = CGAL::Surface_mesh<Kernel::Point_3>;
inline CGALMesh trimesh_to_cgal(const TriMesh& mesh) {
CGALMesh cm;
std::vector<CGALMesh::Vertex_index> vmap(mesh.vertices.size());
for (size_t i = 0; i < mesh.vertices.size(); ++i)
vmap[i] = cm.add_vertex(Kernel::Point_3(mesh.vertices[i].x(), mesh.vertices[i].y(), mesh.vertices[i].z()));
for (const auto& f : mesh.indices) {
cm.add_face(vmap[f[0]], vmap[f[1]], vmap[f[2]]);
}
return cm;
}
inline TriMesh cgal_to_trimesh(const CGALMesh& cm) {
TriMesh mesh;
std::map<CGALMesh::Vertex_index, size_t> vmap;
size_t idx = 0;
for (auto v : cm.vertices()) {
if (!cm.is_valid(v) || cm.is_removed(v)) continue;
auto p = cm.point(v);
mesh.vertices.push_back(Vec3f((float)p.x(), (float)p.y(), (float)p.z()));
vmap[v] = idx++;
}
for (auto f : cm.faces()) {
if (!cm.is_valid(f) || cm.is_removed(f)) continue;
auto h = cm.halfedge(f);
auto v0 = cm.target(h);
auto v1 = cm.target(cm.next(h));
auto v2 = cm.target(cm.next(cm.next(h)));
mesh.indices.push_back(Vec3i32((int)vmap[v0], (int)vmap[v1], (int)vmap[v2]));
}
return mesh;
}
inline bool is_mesh_halfedge_compatible(const TriMesh& mesh) {
std::vector<std::unordered_set<std::size_t>> vtx_to_adj_faces(mesh.vertices.size());
std::size_t edge_id = 0;
std::vector<std::unordered_set<std::size_t>> edge_to_faces;
std::vector<std::unordered_set<std::size_t>> vtx_to_prev_vtxs(mesh.vertices.size());
std::vector<std::unordered_set<std::size_t>> vtx_to_next_vtxs(mesh.vertices.size());
std::vector<std::unordered_map<std::size_t, std::size_t>> vtx_vtx_to_edge(mesh.vertices.size());
for (std::size_t fid = 0; fid < mesh.indices.size(); ++fid) {
const TriFace& face = mesh.indices[fid];
if (face[0] == face[1] || face[1] == face[2] || face[2] == face[0]) {
return false;
}
for (std::size_t i = 0; i < 3; ++i) {
if (static_cast<std::size_t>(face[i]) >= mesh.vertices.size()) {
return false;
}
vtx_to_adj_faces[face[i]].insert(fid);
std::size_t prev_vtx = face[(i + 2) % 3];
std::size_t next_vtx = face[(i + 1) % 3];
if (vtx_to_prev_vtxs[face[i]].count(prev_vtx)) {
return false;
}
vtx_to_prev_vtxs[face[i]].insert(prev_vtx);
if (vtx_to_next_vtxs[face[i]].count(next_vtx)) {
return false;
}
vtx_to_next_vtxs[face[i]].insert(next_vtx);
}
for (std::size_t i = 0; i < 3; ++i) {
std::size_t va = face[i];
std::size_t vb = face[(i + 1) % 3];
if (!vtx_vtx_to_edge[va].count(vb)) {
vtx_vtx_to_edge[va][vb] = edge_id;
vtx_vtx_to_edge[vb][va] = edge_id;
++edge_id;
edge_to_faces.emplace_back(std::unordered_set<std::size_t>());
}
edge_to_faces[vtx_vtx_to_edge[va][vb]].insert(fid);
}
}
for (std::size_t vid = 0; vid < mesh.vertices.size(); ++vid) {
if (vtx_to_adj_faces[vid].empty()) {
continue;
}
std::unordered_set<std::size_t> visited_faces;
std::queue<std::size_t> face_queue;
face_queue.push(*(vtx_to_adj_faces[vid].begin()));
visited_faces.insert(*(vtx_to_adj_faces[vid].begin()));
while (!face_queue.empty()) {
std::size_t fid = face_queue.front();
face_queue.pop();
const TriFace& face = mesh.indices[fid];
for (std::size_t i = 0; i < 3; ++i) {
if (static_cast<std::size_t>(face[i]) != vid) {
continue;
}
std::size_t v_next = face[(i + 1) % 3];
std::size_t v_prev = face[(i + 2) % 3];
for (std::size_t nbr : {v_next, v_prev}) {
std::size_t eid = vtx_vtx_to_edge[vid][nbr];
for (std::size_t adj_fid : edge_to_faces[eid]) {
if (!visited_faces.count(adj_fid) && vtx_to_adj_faces[vid].count(adj_fid)) {
visited_faces.insert(adj_fid);
face_queue.push(adj_fid);
}
}
}
break;
}
}
for (std::size_t fid : vtx_to_adj_faces[vid]) {
if (!visited_faces.count(fid)) {
return false;
}
}
}
return true;
}
inline bool convert_trimesh_to_cgal(const TriMesh& mesh, CGALMesh& cgal_mesh) {
cgal_mesh = trimesh_to_cgal(mesh);
return cgal_mesh.number_of_faces() > 0 || mesh.indices.empty();
}
inline bool convert_trimesh_to_cgal(
const TriMesh& mesh, const std::vector<Vec2f>& vertex_uvs,
CGALMesh& cgal_mesh, std::vector<Vec2f>& cgal_vertex_uvs)
{
cgal_mesh.clear();
std::vector<CGALMesh::Vertex_index> vmap(mesh.vertices.size());
cgal_vertex_uvs.clear();
for (size_t i = 0; i < mesh.vertices.size(); ++i) {
vmap[i] = cgal_mesh.add_vertex(Kernel::Point_3(
mesh.vertices[i].x(), mesh.vertices[i].y(), mesh.vertices[i].z()));
}
cgal_vertex_uvs.resize(cgal_mesh.num_vertices());
for (size_t i = 0; i < mesh.vertices.size(); ++i) {
if (i < vertex_uvs.size())
cgal_vertex_uvs[vmap[i]] = vertex_uvs[i];
else
cgal_vertex_uvs[vmap[i]] = Vec2f(0.f, 0.f);
}
for (const auto& f : mesh.indices)
cgal_mesh.add_face(vmap[f[0]], vmap[f[1]], vmap[f[2]]);
return true;
}
} // namespace cgalutils
} // namespace tex2color
} // namespace Slic3r
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More